Decompiled source of GsStatsEmitter v0.8.1

GsStatsEmitter.dll

Decompiled 6 hours ago
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HG;
using Microsoft.CodeAnalysis;
using RoR2;
using RoR2.Stats;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("GsStatsEmitter")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.8.1.0")]
[assembly: AssemblyInformationalVersion("0.8.1+08294f4a1aab1e5fe5cdfc4482b23ad7cd703bba")]
[assembly: AssemblyProduct("GsStatsEmitter")]
[assembly: AssemblyTitle("GsStatsEmitter")]
[assembly: AssemblyVersion("0.8.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace GsStatsEmitter
{
	[BepInPlugin("net.cproudlock.gsstatsemitter", "GS Stats Emitter", "0.10.0")]
	public class Plugin : BaseUnityPlugin
	{
		private class StageEntry
		{
			public int index;

			public string scene;

			public double startedAtSec;

			public double endedAtSec;

			public Dictionary<string, Dictionary<string, double>> playerDeltas = new Dictionary<string, Dictionary<string, double>>();

			public Dictionary<string, Dictionary<string, int>> playerItemPicks = new Dictionary<string, Dictionary<string, int>>();
		}

		public const string GUID = "net.cproudlock.gsstatsemitter";

		public const string NAME = "GS Stats Emitter";

		public const string VERSION = "0.10.0";

		private readonly Dictionary<string, Dictionary<string, double>> finalReportStatsByName = new Dictionary<string, Dictionary<string, double>>();

		private readonly Dictionary<string, Dictionary<string, ulong>> finalReportBossKillsByName = new Dictionary<string, Dictionary<string, ulong>>();

		private readonly Dictionary<string, Dictionary<string, double>> lastBodySnapshot = new Dictionary<string, Dictionary<string, double>>();

		private const float HeartbeatIntervalSec = 30f;

		private float lastHeartbeatTime;

		private static readonly string[] StatNames = new string[29]
		{
			"totalKills", "totalEliteKills", "totalDeaths", "totalDamageDealt", "totalDamageTaken", "totalHealthHealed", "totalGoldCollected", "totalItemsCollected", "highestLevel", "totalDistanceTraveled",
			"totalTimeAlive", "totalStagesCompleted", "totalPurchases", "totalMinionDamageDealt", "totalMinionKills", "totalDamageBlocked", "totalGoldPurchases", "totalTeleporterBossKillsWitnessed", "highestDamageDealt", "highestCollected",
			"highestPurchases", "highestGoldPurchases", "highestLunarPurchases", "highestBloodPurchases", "highestStagesCompleted", "highestItemsCollected", "highestTier1Purchases", "highestTier2Purchases", "highestTier3Purchases"
		};

		private static readonly string[] StagedStatNames = new string[8] { "totalKills", "totalEliteKills", "totalDeaths", "totalDamageDealt", "totalDamageTaken", "totalHealthHealed", "totalGoldCollected", "totalItemsCollected" };

		private static readonly string[] BossBodies = new string[26]
		{
			"TitanGoldBody", "TitanBlackBody", "TitanBody", "BeetleQueen2Body", "ClayBossBody", "VagrantBody", "GravekeeperBody", "RoboBallBossBody", "MagmaWormBody", "ElectricWormBody",
			"ImpBossBody", "GrandparentBody", "MithrixBody", "BrotherBody", "BrotherHurtBody", "VoidRaidCrabBody", "VoidRaidCrabJointBody", "ScavLunar1Body", "ScavLunar2Body", "ScavLunar3Body",
			"ScavLunar4Body", "ArtifactShellBody", "FalseSonBoss1Body", "FalseSonBoss2Body", "FalseSonBossLightningBody", "HalcyoniteBody"
		};

		internal static ManualLogSource Log;

		private static readonly HttpClient http = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(5.0)
		};

		private ConfigEntry<string> ingestUrlCfg;

		private ConfigEntry<string> ingestTokenCfg;

		private DateTime runStartUtc;

		private string runIdLocal;

		private readonly List<StageEntry> stages = new List<StageEntry>();

		private StageEntry currentStage;

		private readonly Dictionary<string, Dictionary<string, double>> lastSnapshot = new Dictionary<string, Dictionary<string, double>>();

		private readonly Dictionary<string, Dictionary<string, int>> lastItemSnapshot = new Dictionary<string, Dictionary<string, int>>();

		private readonly Dictionary<string, string> lastAttackerByPlayer = new Dictionary<string, string>();

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			ingestUrlCfg = ((BaseUnityPlugin)this).Config.Bind<string>("Ingest", "Url", "http://localhost:3001/api/ingest", "Endpoint to POST run summaries to. Set this if the gs dashboard runs on a different host/port.");
			ingestTokenCfg = ((BaseUnityPlugin)this).Config.Bind<string>("Ingest", "Token", "", "Bearer token required by the gs dashboard. Get this from the dashboard owner.");
			Run.onRunStartGlobal += OnRunStart;
			Run.onRunDestroyGlobal += OnRunDestroy;
			Run.onClientGameOverGlobal += OnClientGameOver;
			Stage.onServerStageBegin += OnServerStageBegin;
			GlobalEventManager.onCharacterDeathGlobal += OnCharacterDeath;
		}

		private void Update()
		{
			if (Time.unscaledTime - lastHeartbeatTime < 30f)
			{
				return;
			}
			lastHeartbeatTime = Time.unscaledTime;
			try
			{
				if (NetworkServer.active)
				{
					Run instance = Run.instance;
					if (!((Object)(object)instance == (Object)null))
					{
						SendHeartbeat(instance);
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] heartbeat: " + ex.Message));
			}
		}

		private void SendHeartbeat(Run run)
		{
			//IL_013c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0141: Unknown result type (might be due to invalid IL or missing references)
			StringBuilder stringBuilder = new StringBuilder(512);
			stringBuilder.Append('{');
			J(stringBuilder, "schemaVersion", 1);
			C(stringBuilder);
			J(stringBuilder, "game", "ror2");
			C(stringBuilder);
			J(stringBuilder, "runIdLocal", runIdLocal ?? "");
			C(stringBuilder);
			string val = "";
			string val2 = "";
			try
			{
				foreach (PlayerCharacterMasterController instance in PlayerCharacterMasterController.instances)
				{
					if (!((Object)(object)((instance != null) ? instance.networkUser : null) == (Object)null) && ((NetworkBehaviour)instance.networkUser).isLocalPlayer)
					{
						val = instance.GetDisplayName() ?? "";
						CharacterMaster master = instance.master;
						object obj;
						if (master == null)
						{
							obj = null;
						}
						else
						{
							GameObject bodyPrefab = master.bodyPrefab;
							obj = ((bodyPrefab != null) ? bodyPrefab.GetComponent<CharacterBody>() : null);
						}
						CharacterBody val3 = (CharacterBody)obj;
						if ((Object)(object)val3 != (Object)null)
						{
							val2 = val3.baseNameToken ?? "";
						}
						break;
					}
				}
			}
			catch
			{
			}
			J(stringBuilder, "hostName", val);
			C(stringBuilder);
			J(stringBuilder, "character", val2);
			C(stringBuilder);
			DifficultyIndex selectedDifficulty = run.selectedDifficulty;
			J(stringBuilder, "difficulty", ((object)(DifficultyIndex)(ref selectedDifficulty)).ToString());
			C(stringBuilder);
			string val4 = "unknown";
			try
			{
				SceneDef sceneDefForCurrentScene = SceneCatalog.GetSceneDefForCurrentScene();
				val4 = ((sceneDefForCurrentScene != null) ? sceneDefForCurrentScene.cachedName : null) ?? "unknown";
			}
			catch
			{
			}
			J(stringBuilder, "scene", val4);
			C(stringBuilder);
			J(stringBuilder, "startedAtUtc", runStartUtc.ToString("O"));
			C(stringBuilder);
			J(stringBuilder, "elapsedSec", Mathf.RoundToInt(run.GetRunStopwatch()));
			C(stringBuilder);
			J(stringBuilder, "stageClearCount", run.stageClearCount);
			C(stringBuilder);
			J(stringBuilder, "eclipseLevel", GetEclipseLevel(run));
			stringBuilder.Append('}');
			try
			{
				StringContent content = new StringContent(stringBuilder.ToString(), Encoding.UTF8, "application/json");
				string requestUri = ingestUrlCfg.Value.Replace("/api/ingest", "/api/heartbeat");
				HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri)
				{
					Content = content
				};
				if (!string.IsNullOrEmpty(ingestTokenCfg.Value))
				{
					httpRequestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ingestTokenCfg.Value);
				}
				http.SendAsync(httpRequestMessage).ContinueWith(delegate(Task<HttpResponseMessage> t)
				{
					if (t.IsFaulted)
					{
						Log.LogWarning((object)("[gs] heartbeat send: " + t.Exception?.GetBaseException().Message));
					}
				});
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] heartbeat post: " + ex.Message));
			}
			Log.LogInfo((object)("GS Stats Emitter 0.10.0 loaded, posting to " + ingestUrlCfg.Value + " (token " + (string.IsNullOrEmpty(ingestTokenCfg.Value) ? "MISSING" : "set") + ")"));
		}

		private void OnRunStart(Run run)
		{
			runStartUtc = DateTime.UtcNow;
			runIdLocal = Guid.NewGuid().ToString("N");
			stages.Clear();
			currentStage = null;
			lastSnapshot.Clear();
			lastItemSnapshot.Clear();
			lastAttackerByPlayer.Clear();
			lastBodySnapshot.Clear();
			finalReportStatsByName.Clear();
			finalReportBossKillsByName.Clear();
			Log.LogInfo((object)$"[gs] run started, seed={run.seed}, host={NetworkServer.active}, runId={runIdLocal}");
		}

		private void OnClientGameOver(Run run, RunReport report)
		{
			if (report == null)
			{
				Log.LogInfo((object)"[gs] game over: null report");
				return;
			}
			try
			{
				int playerInfoCount = report.playerInfoCount;
				Log.LogInfo((object)$"[gs] game over, report has {playerInfoCount} player(s)");
				List<StatDef> allStatDefs = StatDef.allStatDefs;
				for (int i = 0; i < playerInfoCount; i++)
				{
					PlayerInfo playerInfo = report.GetPlayerInfo(i);
					if (playerInfo == null)
					{
						continue;
					}
					string text = SafeReportName(playerInfo);
					StatSheet val = TryGetStatSheet(playerInfo);
					if (val == null)
					{
						Log.LogInfo((object)("[gs]   '" + text + "': no statSheet"));
						continue;
					}
					Dictionary<string, double> dictionary = new Dictionary<string, double>();
					string[] statNames = StatNames;
					foreach (string text2 in statNames)
					{
						StatDef val2 = null;
						for (int k = 0; k < allStatDefs.Count; k++)
						{
							if (allStatDefs[k] != null && allStatDefs[k].name == text2)
							{
								val2 = allStatDefs[k];
								break;
							}
						}
						if (val2 != null)
						{
							double num = ReadStat(val, val2);
							if (num > 0.0)
							{
								dictionary[text2] = num;
							}
						}
					}
					finalReportStatsByName[text] = dictionary;
					Dictionary<string, ulong> dictionary2 = new Dictionary<string, ulong>();
					statNames = BossBodies;
					foreach (string text3 in statNames)
					{
						string text4 = "killsAgainst." + text3;
						StatDef val3 = null;
						for (int l = 0; l < allStatDefs.Count; l++)
						{
							if (allStatDefs[l] != null && allStatDefs[l].name == text4)
							{
								val3 = allStatDefs[l];
								break;
							}
						}
						if (val3 != null)
						{
							ulong num2 = (ulong)ReadStat(val, val3);
							if (num2 != 0)
							{
								dictionary2[text3] = num2;
							}
						}
					}
					finalReportBossKillsByName[text] = dictionary2;
					Log.LogInfo((object)$"[gs]   '{text}': {dictionary.Count} stats, {dictionary2.Count} boss-kill entries");
				}
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"[gs] game over capture: {arg}");
			}
		}

		private static string SafeReportName(PlayerInfo pi)
		{
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (!string.IsNullOrEmpty(pi.name))
				{
					return pi.name;
				}
			}
			catch
			{
			}
			try
			{
				NetworkUser networkUser = pi.networkUser;
				if ((Object)(object)networkUser != (Object)null)
				{
					try
					{
						string userName = networkUser.userName;
						if (!string.IsNullOrEmpty(userName))
						{
							return userName;
						}
					}
					catch
					{
					}
					try
					{
						NetworkPlayerName networkPlayerName = networkUser.GetNetworkPlayerName();
						string resolvedName = ((NetworkPlayerName)(ref networkPlayerName)).GetResolvedName();
						if (!string.IsNullOrEmpty(resolvedName))
						{
							return resolvedName;
						}
					}
					catch
					{
					}
				}
			}
			catch
			{
			}
			return "unknown";
		}

		private static StatSheet TryGetStatSheet(PlayerInfo pi)
		{
			try
			{
				return pi.statSheet;
			}
			catch
			{
				return null;
			}
		}

		private static double ReadStat(StatSheet sheet, StatDef def)
		{
			if (sheet == null || def == null)
			{
				return 0.0;
			}
			try
			{
				ulong statValueULong = sheet.GetStatValueULong(def);
				if (statValueULong != 0L)
				{
					return statValueULong;
				}
			}
			catch
			{
			}
			try
			{
				return sheet.GetStatValueDouble(def);
			}
			catch
			{
				return 0.0;
			}
		}

		private void SnapshotBody(string playerName, CharacterBody body)
		{
			Dictionary<string, double> snap;
			if (!((Object)(object)body == (Object)null) && !string.IsNullOrEmpty(playerName))
			{
				snap = new Dictionary<string, double>();
				Try("level", () => body.level);
				Try("maxHealth", () => body.maxHealth);
				Try("maxShield", () => body.maxShield);
				Try("regen", () => body.regen);
				Try("damage", () => body.damage);
				Try("baseDamage", () => body.baseDamage);
				Try("attackSpeed", () => body.attackSpeed);
				Try("moveSpeed", () => body.moveSpeed);
				Try("jumpPower", () => body.jumpPower);
				Try("crit", () => body.crit);
				Try("armor", () => body.armor);
				Try("maxJumpCount", () => body.maxJumpCount);
				lastBodySnapshot[playerName] = snap;
			}
			void Try(string k, Func<double> read)
			{
				try
				{
					snap[k] = read();
				}
				catch
				{
				}
			}
		}

		private void OnCharacterDeath(DamageReport report)
		{
			if (report == null || (Object)(object)report.victim == (Object)null)
			{
				return;
			}
			try
			{
				CharacterMaster victimMaster = report.victimMaster;
				if ((Object)(object)victimMaster == (Object)null)
				{
					return;
				}
				PlayerCharacterMasterController playerCharacterMasterController = victimMaster.playerCharacterMasterController;
				if ((Object)(object)playerCharacterMasterController == (Object)null)
				{
					return;
				}
				string key = SafePlayerName(playerCharacterMasterController);
				string value = "environment";
				try
				{
					if ((Object)(object)report.attackerBody != (Object)null)
					{
						value = report.attackerBody.baseNameToken ?? ((Object)report.attackerBody).name ?? "unknown";
					}
					else if (report.damageInfo != null && (Object)(object)report.damageInfo.attacker != (Object)null)
					{
						value = report.damageInfo.attacker.GetComponent<CharacterBody>()?.baseNameToken ?? "unknown";
					}
				}
				catch
				{
				}
				lastAttackerByPlayer[key] = value;
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] death track: " + ex.Message));
			}
		}

		private void OnServerStageBegin(Stage stage)
		{
			if (!NetworkServer.active)
			{
				return;
			}
			Run instance = Run.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				double num = 0.0;
				try
				{
					num = instance.GetRunStopwatch();
				}
				catch
				{
				}
				string text = "unknown";
				try
				{
					SceneDef sceneDefForCurrentScene = SceneCatalog.GetSceneDefForCurrentScene();
					text = ((sceneDefForCurrentScene != null) ? sceneDefForCurrentScene.cachedName : null) ?? "unknown";
				}
				catch
				{
				}
				if (currentStage != null)
				{
					currentStage.endedAtSec = num;
					FillStageDeltas(currentStage);
					stages.Add(currentStage);
				}
				currentStage = new StageEntry
				{
					index = stages.Count,
					scene = text,
					startedAtSec = num
				};
				SnapshotAllPlayers();
				Log.LogInfo((object)$"[gs] stage {currentStage.index} ({text}) at {num:0.0}s");
			}
		}

		private void SnapshotAllPlayers()
		{
			//IL_0198: Unknown result type (might be due to invalid IL or missing references)
			//IL_0181: Unknown result type (might be due to invalid IL or missing references)
			//IL_0187: Invalid comparison between Unknown and I4
			//IL_0142: Unknown result type (might be due to invalid IL or missing references)
			//IL_0147: Unknown result type (might be due to invalid IL or missing references)
			//IL_014b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0150: Unknown result type (might be due to invalid IL or missing references)
			lastSnapshot.Clear();
			lastItemSnapshot.Clear();
			ReadOnlyCollection<PlayerCharacterMasterController> instances = PlayerCharacterMasterController.instances;
			if (instances == null)
			{
				return;
			}
			List<StatDef> allStatDefs = StatDef.allStatDefs;
			foreach (PlayerCharacterMasterController item in instances)
			{
				if ((Object)(object)((item != null) ? item.master : null) == (Object)null)
				{
					continue;
				}
				string text = SafePlayerName(item);
				try
				{
					SnapshotBody(text, item.master.GetBody());
				}
				catch
				{
				}
				StatSheet val = item.master.playerStatsComponent?.currentStats;
				if (val != null)
				{
					Dictionary<string, double> dictionary = new Dictionary<string, double>();
					string[] stagedStatNames = StagedStatNames;
					foreach (string text2 in stagedStatNames)
					{
						StatDef val2 = null;
						for (int j = 0; j < allStatDefs.Count; j++)
						{
							if (allStatDefs[j] != null && allStatDefs[j].name == text2)
							{
								val2 = allStatDefs[j];
								break;
							}
						}
						if (val2 != null)
						{
							dictionary[text2] = ReadStat(val, val2);
						}
					}
					lastSnapshot[text] = dictionary;
				}
				Inventory inventory = item.master.inventory;
				if (!((Object)(object)inventory != (Object)null))
				{
					continue;
				}
				Dictionary<string, int> dictionary2 = new Dictionary<string, int>();
				try
				{
					Enumerator<ItemDef> enumerator2 = ItemCatalog.allItemDefs.GetEnumerator();
					try
					{
						while (enumerator2.MoveNext())
						{
							ItemDef current2 = enumerator2.Current;
							if ((Object)(object)current2 == (Object)null)
							{
								continue;
							}
							bool flag = false;
							try
							{
								flag = current2.hidden;
							}
							catch
							{
							}
							if (flag)
							{
								continue;
							}
							bool flag2 = false;
							try
							{
								flag2 = (int)current2.tier == 5;
							}
							catch
							{
							}
							if (!flag2)
							{
								int itemCountPermanent = inventory.GetItemCountPermanent(current2.itemIndex);
								if (itemCountPermanent > 0)
								{
									dictionary2[((Object)current2).name] = itemCountPermanent;
								}
							}
						}
					}
					finally
					{
						((IDisposable)enumerator2).Dispose();
					}
				}
				catch
				{
				}
				lastItemSnapshot[text] = dictionary2;
			}
		}

		private void FillStageDeltas(StageEntry stage)
		{
			//IL_01c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b6: Invalid comparison between Unknown and I4
			//IL_016e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Unknown result type (might be due to invalid IL or missing references)
			//IL_0177: Unknown result type (might be due to invalid IL or missing references)
			//IL_017c: Unknown result type (might be due to invalid IL or missing references)
			ReadOnlyCollection<PlayerCharacterMasterController> instances = PlayerCharacterMasterController.instances;
			if (instances == null)
			{
				return;
			}
			List<StatDef> allStatDefs = StatDef.allStatDefs;
			foreach (PlayerCharacterMasterController item in instances)
			{
				if ((Object)(object)((item != null) ? item.master : null) == (Object)null)
				{
					continue;
				}
				string key = SafePlayerName(item);
				StatSheet val = item.master.playerStatsComponent?.currentStats;
				if (val != null)
				{
					lastSnapshot.TryGetValue(key, out var value);
					Dictionary<string, double> dictionary = new Dictionary<string, double>();
					string[] stagedStatNames = StagedStatNames;
					foreach (string text in stagedStatNames)
					{
						StatDef val2 = null;
						for (int j = 0; j < allStatDefs.Count; j++)
						{
							if (allStatDefs[j] != null && allStatDefs[j].name == text)
							{
								val2 = allStatDefs[j];
								break;
							}
						}
						if (val2 != null)
						{
							double num = ReadStat(val, val2);
							double value2;
							double num2 = ((value != null && value.TryGetValue(text, out value2)) ? value2 : 0.0);
							double num3 = num - num2;
							if (num3 > 0.0)
							{
								dictionary[text] = num3;
							}
						}
					}
					stage.playerDeltas[key] = dictionary;
				}
				Inventory inventory = item.master.inventory;
				if (!((Object)(object)inventory != (Object)null))
				{
					continue;
				}
				lastItemSnapshot.TryGetValue(key, out var value3);
				Dictionary<string, int> dictionary2 = new Dictionary<string, int>();
				try
				{
					Enumerator<ItemDef> enumerator2 = ItemCatalog.allItemDefs.GetEnumerator();
					try
					{
						while (enumerator2.MoveNext())
						{
							ItemDef current2 = enumerator2.Current;
							if ((Object)(object)current2 == (Object)null)
							{
								continue;
							}
							bool flag = false;
							try
							{
								flag = current2.hidden;
							}
							catch
							{
							}
							if (flag)
							{
								continue;
							}
							bool flag2 = false;
							try
							{
								flag2 = (int)current2.tier == 5;
							}
							catch
							{
							}
							if (!flag2)
							{
								int itemCountPermanent = inventory.GetItemCountPermanent(current2.itemIndex);
								int value4;
								int num4 = ((value3 != null && value3.TryGetValue(((Object)current2).name, out value4)) ? value4 : 0);
								int num5 = itemCountPermanent - num4;
								if (num5 > 0)
								{
									dictionary2[((Object)current2).name] = num5;
								}
							}
						}
					}
					finally
					{
						((IDisposable)enumerator2).Dispose();
					}
				}
				catch
				{
				}
				if (dictionary2.Count > 0)
				{
					stage.playerItemPicks[key] = dictionary2;
				}
			}
		}

		private static string SafePlayerName(PlayerCharacterMasterController pcmc)
		{
			try
			{
				string displayName = pcmc.GetDisplayName();
				if (!string.IsNullOrEmpty(displayName))
				{
					return displayName;
				}
			}
			catch
			{
			}
			try
			{
				return ((Object)pcmc.master).name ?? "unknown";
			}
			catch
			{
			}
			return "unknown";
		}

		private void OnRunDestroy(Run run)
		{
			if ((Object)(object)run == (Object)null)
			{
				return;
			}
			if (!NetworkServer.active)
			{
				Log.LogInfo((object)"[gs] not host, skipping emit");
				return;
			}
			try
			{
				if (currentStage != null)
				{
					double endedAtSec = 0.0;
					try
					{
						endedAtSec = run.GetRunStopwatch();
					}
					catch
					{
					}
					currentStage.endedAtSec = endedAtSec;
					FillStageDeltas(currentStage);
					stages.Add(currentStage);
					currentStage = null;
				}
				string text = BuildJson(run);
				Log.LogInfo((object)$"[gs] emit ({text.Length} bytes, {stages.Count} stages)");
				StringContent content = new StringContent(text, Encoding.UTF8, "application/json");
				HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, ingestUrlCfg.Value)
				{
					Content = content
				};
				if (!string.IsNullOrEmpty(ingestTokenCfg.Value))
				{
					httpRequestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + ingestTokenCfg.Value);
				}
				http.SendAsync(httpRequestMessage).ContinueWith(delegate(Task<HttpResponseMessage> t)
				{
					if (t.IsFaulted)
					{
						Log.LogError((object)("[gs] ingest failed: " + t.Exception?.GetBaseException().Message));
					}
					else
					{
						Log.LogInfo((object)$"[gs] ingest status: {(int)t.Result.StatusCode}");
					}
				});
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"[gs] emit error: {arg}");
			}
		}

		private string BuildJson(Run run)
		{
			//IL_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			StringBuilder stringBuilder = new StringBuilder(2048);
			stringBuilder.Append('{');
			J(stringBuilder, "schemaVersion", 1);
			C(stringBuilder);
			J(stringBuilder, "game", "ror2");
			C(stringBuilder);
			J(stringBuilder, "runIdLocal", runIdLocal);
			C(stringBuilder);
			J(stringBuilder, "seed", run.seed.ToString());
			C(stringBuilder);
			DifficultyIndex selectedDifficulty = run.selectedDifficulty;
			J(stringBuilder, "difficulty", ((object)(DifficultyIndex)(ref selectedDifficulty)).ToString());
			C(stringBuilder);
			J(stringBuilder, "stageClearCount", run.stageClearCount);
			C(stringBuilder);
			J(stringBuilder, "runTimeSeconds", Mathf.RoundToInt(run.GetRunStopwatch()));
			C(stringBuilder);
			J(stringBuilder, "startedAtUtc", runStartUtc.ToString("O"));
			C(stringBuilder);
			J(stringBuilder, "endedAtUtc", DateTime.UtcNow.ToString("O"));
			C(stringBuilder);
			string val = "unknown";
			try
			{
				SceneDef sceneDefForCurrentScene = SceneCatalog.GetSceneDefForCurrentScene();
				val = ((sceneDefForCurrentScene != null) ? sceneDefForCurrentScene.cachedName : null) ?? "unknown";
			}
			catch
			{
			}
			J(stringBuilder, "endedOnScene", val);
			C(stringBuilder);
			J(stringBuilder, "eclipseLevel", GetEclipseLevel(run));
			C(stringBuilder);
			stringBuilder.Append("\"artifacts\":[");
			AppendArtifacts(stringBuilder);
			stringBuilder.Append("],");
			stringBuilder.Append("\"itemTiers\":{");
			AppendItemTiers(stringBuilder);
			stringBuilder.Append("},");
			stringBuilder.Append("\"mods\":[");
			AppendMods(stringBuilder);
			stringBuilder.Append("],");
			stringBuilder.Append("\"stages\":[");
			AppendStages(stringBuilder);
			stringBuilder.Append("],");
			stringBuilder.Append("\"players\":[");
			bool flag = true;
			ReadOnlyCollection<PlayerCharacterMasterController> instances = PlayerCharacterMasterController.instances;
			if (instances != null)
			{
				foreach (PlayerCharacterMasterController item in instances)
				{
					if (!((Object)(object)item == (Object)null) && !((Object)(object)item.master == (Object)null))
					{
						if (!flag)
						{
							stringBuilder.Append(',');
						}
						flag = false;
						AppendPlayer(stringBuilder, item);
					}
				}
			}
			stringBuilder.Append(']');
			stringBuilder.Append('}');
			return stringBuilder.ToString();
		}

		private void AppendPlayer(StringBuilder sb, PlayerCharacterMasterController pcmc)
		{
			//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_019d: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a3: Invalid comparison between Unknown and I4
			//IL_015b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_0164: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Unknown result type (might be due to invalid IL or missing references)
			string text = "unknown";
			try
			{
				text = pcmc.GetDisplayName() ?? ((Object)pcmc.master).name;
			}
			catch
			{
				try
				{
					text = ((Object)pcmc.master).name;
				}
				catch
				{
				}
			}
			string val = "unknown";
			try
			{
				string text2 = ((!((Object)(object)pcmc.master.bodyPrefab != (Object)null)) ? null : pcmc.master.bodyPrefab.GetComponent<CharacterBody>()?.baseNameToken);
				if (!string.IsNullOrEmpty(text2))
				{
					val = text2;
				}
			}
			catch
			{
			}
			bool flag = false;
			try
			{
				flag = pcmc.master.IsDeadAndOutOfLivesServer();
			}
			catch
			{
			}
			bool val2 = false;
			try
			{
				val2 = (Object)(object)pcmc.networkUser != (Object)null && ((NetworkBehaviour)pcmc.networkUser).isLocalPlayer;
			}
			catch
			{
			}
			string val3 = null;
			if (flag && lastAttackerByPlayer.TryGetValue(text, out var value))
			{
				val3 = value;
			}
			sb.Append('{');
			J(sb, "name", text);
			C(sb);
			J(sb, "character", val);
			C(sb);
			J(sb, "dead", flag);
			C(sb);
			J(sb, "isHost", val2);
			C(sb);
			J(sb, "killedBy", val3);
			C(sb);
			sb.Append("\"items\":{");
			bool flag2 = true;
			try
			{
				Inventory inventory = pcmc.master.inventory;
				if ((Object)(object)inventory != (Object)null)
				{
					Enumerator<ItemDef> enumerator = ItemCatalog.allItemDefs.GetEnumerator();
					try
					{
						while (enumerator.MoveNext())
						{
							ItemDef current = enumerator.Current;
							if ((Object)(object)current == (Object)null)
							{
								continue;
							}
							bool flag3 = false;
							try
							{
								flag3 = current.hidden;
							}
							catch
							{
							}
							if (flag3)
							{
								continue;
							}
							bool flag4 = false;
							try
							{
								flag4 = (int)current.tier == 5;
							}
							catch
							{
							}
							if (flag4)
							{
								continue;
							}
							int itemCountPermanent = inventory.GetItemCountPermanent(current.itemIndex);
							if (itemCountPermanent > 0)
							{
								if (!flag2)
								{
									sb.Append(',');
								}
								flag2 = false;
								sb.Append('"').Append(Escape(((Object)current).name)).Append("\":")
									.Append(itemCountPermanent);
							}
						}
					}
					finally
					{
						((IDisposable)enumerator).Dispose();
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] item enum: " + ex.Message));
			}
			sb.Append('}');
			AppendPlayerStats(sb, pcmc);
			AppendBossKills(sb, pcmc);
			AppendBodySnapshot(sb, pcmc);
			AppendLoadout(sb, pcmc);
			sb.Append('}');
		}

		private void AppendLoadout(StringBuilder sb, PlayerCharacterMasterController pcmc)
		{
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_004b: Invalid comparison between Unknown and I4
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			CharacterBody val = null;
			try
			{
				CharacterMaster master = pcmc.master;
				val = ((master != null) ? master.GetBody() : null);
			}
			catch
			{
			}
			string text = null;
			try
			{
				CharacterMaster master2 = pcmc.master;
				Inventory val2 = ((master2 != null) ? master2.inventory : null);
				if ((Object)(object)val2 != (Object)null)
				{
					EquipmentIndex equipmentIndex = val2.GetEquipmentIndex();
					if ((int)equipmentIndex != -1)
					{
						EquipmentDef equipmentDef = EquipmentCatalog.GetEquipmentDef(equipmentIndex);
						if ((Object)(object)equipmentDef != (Object)null)
						{
							text = ((Object)equipmentDef).name;
						}
					}
				}
			}
			catch
			{
			}
			sb.Append(",\"equipment\":");
			if (text != null)
			{
				sb.Append('"').Append(Escape(text)).Append('"');
			}
			else
			{
				sb.Append("null");
			}
			sb.Append(",\"skills\":{");
			try
			{
				SkillLocator val3 = ((val != null) ? val.skillLocator : null);
				bool firstSlot;
				if ((Object)(object)val3 != (Object)null)
				{
					firstSlot = true;
					EmitSlot("primary", val3.primary);
					EmitSlot("secondary", val3.secondary);
					EmitSlot("utility", val3.utility);
					EmitSlot("special", val3.special);
				}
				void EmitSlot(string slot, GenericSkill gs)
				{
					if (!((Object)(object)gs == (Object)null))
					{
						string text2 = null;
						try
						{
							text2 = gs.skillDef?.skillName ?? gs.skillDef?.skillNameToken;
						}
						catch
						{
						}
						if (!string.IsNullOrEmpty(text2))
						{
							if (!firstSlot)
							{
								sb.Append(',');
							}
							firstSlot = false;
							sb.Append('"').Append(slot).Append("\":\"")
								.Append(Escape(text2))
								.Append('"');
						}
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] skill enum: " + ex.Message));
			}
			sb.Append('}');
		}

		private void AppendBodySnapshot(StringBuilder sb, PlayerCharacterMasterController pcmc)
		{
			sb.Append(",\"finalStats\":{");
			string text = SafePlayerName(pcmc);
			try
			{
				CharacterMaster master = pcmc.master;
				SnapshotBody(text, (master != null) ? master.GetBody() : null);
			}
			catch
			{
			}
			if (!lastBodySnapshot.TryGetValue(text, out var value) || value == null || value.Count == 0)
			{
				sb.Append('}');
				return;
			}
			bool flag = true;
			foreach (KeyValuePair<string, double> item in value)
			{
				double value2 = item.Value;
				if (!double.IsNaN(value2) && !double.IsInfinity(value2))
				{
					if (!flag)
					{
						sb.Append(',');
					}
					flag = false;
					sb.Append('"').Append(item.Key).Append("\":");
					if (Math.Floor(value2) == value2 && Math.Abs(value2) < 1000000000000000.0)
					{
						sb.Append((long)value2);
					}
					else
					{
						sb.Append(value2.ToString("0.##", CultureInfo.InvariantCulture));
					}
				}
			}
			sb.Append('}');
		}

		private void AppendStages(StringBuilder sb)
		{
			bool flag = true;
			foreach (StageEntry stage in stages)
			{
				if (!flag)
				{
					sb.Append(',');
				}
				flag = false;
				sb.Append('{');
				J(sb, "index", stage.index);
				C(sb);
				J(sb, "scene", stage.scene ?? "unknown");
				C(sb);
				sb.Append("\"startedAtSec\":").Append(stage.startedAtSec.ToString("0.##", CultureInfo.InvariantCulture));
				C(sb);
				sb.Append("\"endedAtSec\":").Append(stage.endedAtSec.ToString("0.##", CultureInfo.InvariantCulture));
				C(sb);
				sb.Append("\"playerDeltas\":{");
				bool flag2 = true;
				foreach (KeyValuePair<string, Dictionary<string, double>> playerDelta in stage.playerDeltas)
				{
					if (!flag2)
					{
						sb.Append(',');
					}
					flag2 = false;
					sb.Append('"').Append(Escape(playerDelta.Key)).Append("\":{");
					bool flag3 = true;
					foreach (KeyValuePair<string, double> item in playerDelta.Value)
					{
						if (!flag3)
						{
							sb.Append(',');
						}
						flag3 = false;
						sb.Append('"').Append(Escape(item.Key)).Append("\":");
						double value = item.Value;
						if (Math.Floor(value) == value && Math.Abs(value) < 1000000000000000.0)
						{
							sb.Append((long)value);
						}
						else
						{
							sb.Append(value.ToString("0.##", CultureInfo.InvariantCulture));
						}
					}
					sb.Append('}');
				}
				sb.Append('}');
				sb.Append(",\"playerItemPicks\":{");
				bool flag4 = true;
				foreach (KeyValuePair<string, Dictionary<string, int>> playerItemPick in stage.playerItemPicks)
				{
					if (!flag4)
					{
						sb.Append(',');
					}
					flag4 = false;
					sb.Append('"').Append(Escape(playerItemPick.Key)).Append("\":{");
					bool flag5 = true;
					foreach (KeyValuePair<string, int> item2 in playerItemPick.Value)
					{
						if (!flag5)
						{
							sb.Append(',');
						}
						flag5 = false;
						sb.Append('"').Append(Escape(item2.Key)).Append("\":")
							.Append(item2.Value);
					}
					sb.Append('}');
				}
				sb.Append('}');
				sb.Append('}');
			}
		}

		private void AppendMods(StringBuilder sb)
		{
			bool flag = true;
			try
			{
				Dictionary<string, PluginInfo> pluginInfos = Chainloader.PluginInfos;
				if (pluginInfos == null)
				{
					return;
				}
				foreach (KeyValuePair<string, PluginInfo> item in pluginInfos)
				{
					PluginInfo value = item.Value;
					if (value == null || value.Metadata == null)
					{
						continue;
					}
					string text = "";
					string val = "";
					try
					{
						string directoryName = Path.GetDirectoryName(value.Location ?? "");
						if (!string.IsNullOrEmpty(directoryName))
						{
							text = Path.GetFileName(directoryName);
							int num = text.IndexOf('-');
							if (num > 0)
							{
								val = text.Substring(0, num);
							}
						}
					}
					catch
					{
					}
					if (!flag)
					{
						sb.Append(',');
					}
					flag = false;
					sb.Append('{');
					J(sb, "guid", value.Metadata.GUID ?? "");
					C(sb);
					J(sb, "name", value.Metadata.Name ?? "");
					C(sb);
					J(sb, "version", value.Metadata.Version?.ToString() ?? "");
					C(sb);
					J(sb, "author", val);
					C(sb);
					J(sb, "folder", text);
					sb.Append('}');
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] mods enum: " + ex.Message));
			}
		}

		private void AppendItemTiers(StringBuilder sb)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			bool flag = true;
			try
			{
				Enumerator<ItemDef> enumerator = ItemCatalog.allItemDefs.GetEnumerator();
				try
				{
					while (enumerator.MoveNext())
					{
						ItemDef current = enumerator.Current;
						if ((Object)(object)current == (Object)null)
						{
							continue;
						}
						bool flag2 = false;
						try
						{
							flag2 = current.hidden;
						}
						catch
						{
						}
						if (flag2)
						{
							continue;
						}
						string text = null;
						try
						{
							ItemTier tier = current.tier;
							text = ((object)(ItemTier)(ref tier)).ToString();
						}
						catch
						{
						}
						if (!string.IsNullOrEmpty(text) && !(text == "NoTier"))
						{
							if (!flag)
							{
								sb.Append(',');
							}
							flag = false;
							sb.Append('"').Append(Escape(((Object)current).name)).Append("\":\"")
								.Append(Escape(text))
								.Append('"');
						}
					}
				}
				finally
				{
					((IDisposable)enumerator).Dispose();
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] item tiers: " + ex.Message));
			}
		}

		private void AppendArtifacts(StringBuilder sb)
		{
			bool flag = true;
			try
			{
				if ((Object)(object)RunArtifactManager.instance == (Object)null)
				{
					return;
				}
				int artifactCount = ArtifactCatalog.artifactCount;
				for (int i = 0; i < artifactCount; i++)
				{
					ArtifactDef artifactDef = ArtifactCatalog.GetArtifactDef((ArtifactIndex)i);
					if (!((Object)(object)artifactDef == (Object)null) && RunArtifactManager.instance.IsArtifactEnabled(artifactDef))
					{
						if (!flag)
						{
							sb.Append(',');
						}
						flag = false;
						sb.Append('"').Append(Escape(artifactDef.cachedName)).Append('"');
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] artifact enum: " + ex.Message));
			}
		}

		private int GetEclipseLevel(Run run)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				DifficultyIndex selectedDifficulty = run.selectedDifficulty;
				string text = ((object)(DifficultyIndex)(ref selectedDifficulty)).ToString();
				if (text.StartsWith("Eclipse", StringComparison.OrdinalIgnoreCase) && int.TryParse(text.Substring("Eclipse".Length), out var result))
				{
					return result;
				}
			}
			catch
			{
			}
			return 0;
		}

		private void AppendPlayerStats(StringBuilder sb, PlayerCharacterMasterController pcmc)
		{
			sb.Append(",\"stats\":{");
			bool flag = true;
			string key = SafePlayerName(pcmc);
			if (finalReportStatsByName.TryGetValue(key, out var value) && value.Count > 0)
			{
				foreach (KeyValuePair<string, double> item in value)
				{
					if (!flag)
					{
						sb.Append(',');
					}
					flag = false;
					double value2 = item.Value;
					sb.Append('"').Append(Escape(item.Key)).Append("\":");
					if (Math.Floor(value2) == value2 && Math.Abs(value2) < 1000000000000000.0)
					{
						sb.Append((long)value2);
					}
					else
					{
						sb.Append(value2.ToString("0.##", CultureInfo.InvariantCulture));
					}
				}
				sb.Append('}');
				return;
			}
			try
			{
				CharacterMaster master = pcmc.master;
				StatSheet val = ((master == null) ? null : master.playerStatsComponent?.currentStats);
				if (val == null)
				{
					sb.Append('}');
					return;
				}
				List<StatDef> allStatDefs = StatDef.allStatDefs;
				string[] statNames = StatNames;
				foreach (string text in statNames)
				{
					StatDef val2 = null;
					for (int j = 0; j < allStatDefs.Count; j++)
					{
						if (allStatDefs[j] != null && allStatDefs[j].name == text)
						{
							val2 = allStatDefs[j];
							break;
						}
					}
					if (val2 == null)
					{
						continue;
					}
					double num = ReadStat(val, val2);
					if (!(num <= 0.0))
					{
						if (!flag)
						{
							sb.Append(',');
						}
						flag = false;
						sb.Append('"').Append(Escape(text)).Append("\":");
						if (Math.Floor(num) == num && Math.Abs(num) < 1000000000000000.0)
						{
							sb.Append((long)num);
						}
						else
						{
							sb.Append(num.ToString("0.##", CultureInfo.InvariantCulture));
						}
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] stats enum: " + ex.Message));
			}
			sb.Append('}');
		}

		private void AppendBossKills(StringBuilder sb, PlayerCharacterMasterController pcmc)
		{
			sb.Append(",\"bossKills\":{");
			bool flag = true;
			string key = SafePlayerName(pcmc);
			if (finalReportBossKillsByName.TryGetValue(key, out var value) && value.Count > 0)
			{
				foreach (KeyValuePair<string, ulong> item in value)
				{
					if (!flag)
					{
						sb.Append(',');
					}
					flag = false;
					sb.Append('"').Append(Escape(item.Key)).Append("\":")
						.Append(item.Value);
				}
				sb.Append('}');
				return;
			}
			try
			{
				CharacterMaster master = pcmc.master;
				StatSheet val = ((master == null) ? null : master.playerStatsComponent?.currentStats);
				if (val == null)
				{
					sb.Append('}');
					return;
				}
				List<StatDef> allStatDefs = StatDef.allStatDefs;
				string[] bossBodies = BossBodies;
				foreach (string text in bossBodies)
				{
					string text2 = "killsAgainst." + text;
					StatDef val2 = null;
					for (int j = 0; j < allStatDefs.Count; j++)
					{
						if (allStatDefs[j] != null && allStatDefs[j].name == text2)
						{
							val2 = allStatDefs[j];
							break;
						}
					}
					if (val2 == null)
					{
						continue;
					}
					ulong num = (ulong)ReadStat(val, val2);
					if (num != 0L)
					{
						if (!flag)
						{
							sb.Append(',');
						}
						flag = false;
						sb.Append('"').Append(Escape(text)).Append("\":")
							.Append(num);
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[gs] boss enum: " + ex.Message));
			}
			sb.Append('}');
		}

		private static void J(StringBuilder sb, string key, string val)
		{
			sb.Append('"').Append(Escape(key)).Append("\":");
			if (val == null)
			{
				sb.Append("null");
			}
			else
			{
				sb.Append('"').Append(Escape(val)).Append('"');
			}
		}

		private static void J(StringBuilder sb, string key, int val)
		{
			sb.Append('"').Append(Escape(key)).Append("\":")
				.Append(val);
		}

		private static void J(StringBuilder sb, string key, bool val)
		{
			sb.Append('"').Append(Escape(key)).Append("\":")
				.Append(val ? "true" : "false");
		}

		private static void C(StringBuilder sb)
		{
			sb.Append(',');
		}

		private static string Escape(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return s ?? "";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length + 8);
			foreach (char c in s)
			{
				switch (c)
				{
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\r':
					stringBuilder.Append("\\r");
					continue;
				case '\t':
					stringBuilder.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}
	}
}