Decompiled source of ShrineOfBlood v0.1.0

plugins/MuckShrineOfBlood.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]
[assembly: AssemblyCompany("MuckShrineOfBlood")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Adds the Risk of Rain 2 Shrine of Blood. Spend current HP for coins. Never drops you below 1 HP.")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+e84dc69f2eb55ea4293da66d1fed421eede60311")]
[assembly: AssemblyProduct("Shrine of Blood")]
[assembly: AssemblyTitle("MuckShrineOfBlood")]
[assembly: InternalsVisibleTo("MuckShrineOfBlood.Tests")]
[assembly: AssemblyVersion("0.1.0.0")]
namespace MuckShrineOfBlood;

internal sealed class BloodConfig
{
	public readonly ConfigEntry<int> Count;

	public readonly ConfigEntry<float> MinSpacing;

	public readonly ConfigEntry<int> FirstCostPercent;

	public readonly ConfigEntry<float> GoldMultiplier;

	public readonly ConfigEntry<int> MaxUses;

	public readonly ConfigEntry<bool> VerboseLogging;

	public BloodConfig(ConfigFile cfg)
	{
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		//IL_002f: Expected O, but got Unknown
		//IL_005e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0068: Expected O, but got Unknown
		//IL_008d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0097: Expected O, but got Unknown
		//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d0: Expected O, but got Unknown
		//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fd: Expected O, but got Unknown
		Count = cfg.Bind<int>("World", "Count", 2, new ConfigDescription("How many Shrines of Blood to place on grass on each map, including each Wave Run island. 0 places none. On by default. Every player should use the same value.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 16), Array.Empty<object>()));
		MinSpacing = cfg.Bind<float>("World", "MinSpacing", 80f, new ConfigDescription("Minimum distance in meters between shrines. Every player should use the same value.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 500f), Array.Empty<object>()));
		FirstCostPercent = cfg.Bind<int>("Shrine", "FirstCostPercent", 50, new ConfigDescription("Percent of current HP taken on the first use. Each later use costs 1 - (1 - previous)^2, the Risk of Rain 2 curve (50, then 75, then 93.75). The host's value is the one that is paid.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 90), Array.Empty<object>()));
		GoldMultiplier = cfg.Bind<float>("Shrine", "GoldMultiplier", 0.5f, new ConfigDescription("Coins per HP spent, floored. 0.5 is the Risk of Rain 2 rate: half the HP, so 50 HP pays 25 coins. A scrap that floors to 0 still pays 1 coin. The host's value is the one that is paid.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 20f), Array.Empty<object>()));
		MaxUses = cfg.Bind<int>("Shrine", "MaxUses", 3, new ConfigDescription("Uses on each shrine. Risk of Rain 2 allows 3. Every player should use the same value.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 8), Array.Empty<object>()));
		VerboseLogging = cfg.Bind<bool>("Debug", "VerboseLogging", false, "Log each refused ask.");
	}

	public string Code()
	{
		return ConfigStamp.Code(Count.Value, (int)Math.Round(MinSpacing.Value * 100f), FirstCostPercent.Value, (int)Math.Round(GoldMultiplier.Value * 1000f), MaxUses.Value);
	}
}
internal static class BloodHost
{
	public static bool TryAccept(int hostUseIndex, int askUseIndex, float hp, int maxUses, float firstPercent, float goldMultiplier, out BloodQuote quote)
	{
		quote = BloodMath.Quote(hp, askUseIndex, maxUses, firstPercent, goldMultiplier);
		if (askUseIndex != hostUseIndex)
		{
			quote.Ok = false;
			return false;
		}
		return quote.Ok;
	}
}
internal struct BloodQuote
{
	public bool Ok;

	public int UseIndex;

	public float Hp;

	public float Cost;

	public int Gold;

	public float Left;
}
internal static class BloodMath
{
	public const float WikiFirstPercent = 50f;

	public const float WikiGoldPerHp = 0.5f;

	public const int WikiUses = 3;

	public const int DefaultCount = 2;

	public const float DefaultSpacing = 80f;

	public const float MaxTrackedHp = 100000f;

	public const int MaxGold = 1000000;

	public const float PercentCap = 0.99f;

	public static float PercentAt(float firstPercent, int useIndex)
	{
		if (useIndex < 0)
		{
			useIndex = 0;
		}
		double num = (double)firstPercent / 100.0;
		if (double.IsNaN(num) || num < 0.0)
		{
			num = 0.0;
		}
		if (num > 0.9900000095367432)
		{
			num = 0.9900000095367432;
		}
		for (int i = 0; i < useIndex; i++)
		{
			double num2 = 1.0 - num;
			num = 1.0 - num2 * num2;
			if (num > 0.9900000095367432)
			{
				num = 0.9900000095367432;
			}
		}
		return (float)num;
	}

	public static BloodQuote Quote(float hp, int useIndex, int maxUses, float firstPercent, float goldMultiplier)
	{
		BloodQuote result = new BloodQuote
		{
			UseIndex = useIndex,
			Hp = hp
		};
		if (useIndex < 0 || maxUses <= 0 || useIndex >= maxUses)
		{
			return result;
		}
		if (float.IsNaN(hp) || float.IsInfinity(hp) || !(hp > 1f) || hp > 100000f)
		{
			return result;
		}
		double num = (double)hp * (double)PercentAt(firstPercent, useIndex);
		if ((double)hp - num < 1.0)
		{
			num = (double)hp - 1.0;
		}
		if (!(num > 0.0) || double.IsNaN(num) || double.IsInfinity(num))
		{
			return result;
		}
		result.Ok = true;
		result.Cost = (float)num;
		result.Left = (float)((double)hp - num);
		if (result.Left < 1f)
		{
			result.Left = 1f;
		}
		result.Gold = Gold(result.Cost, goldMultiplier);
		return result;
	}

	public static int Gold(float hpSpent, float multiplier)
	{
		if (!(hpSpent > 0f) || !(multiplier > 0f) || float.IsNaN(hpSpent) || float.IsNaN(multiplier))
		{
			return 0;
		}
		if (float.IsInfinity(hpSpent) || float.IsInfinity(multiplier))
		{
			return 1000000;
		}
		double num = (double)hpSpent * (double)multiplier;
		if (double.IsNaN(num) || num <= 0.0)
		{
			return 0;
		}
		int num2 = (int)Math.Floor(num + 1E-06);
		if (num2 < 1)
		{
			num2 = 1;
		}
		if (num2 > 1000000)
		{
			num2 = 1000000;
		}
		return num2;
	}

	public static float Apply(float currentHp, float cost)
	{
		if (float.IsNaN(currentHp) || float.IsInfinity(currentHp))
		{
			return currentHp;
		}
		if (!(currentHp > 1f) || float.IsNaN(cost) || !(cost > 0f))
		{
			return currentHp;
		}
		double num = (double)currentHp - (double)cost;
		if (num < 1.0)
		{
			num = 1.0;
		}
		return (float)num;
	}
}
internal static class BloodPayout
{
	private static bool missingCoin;

	public static bool CanGrant()
	{
		if (ItemManager.Instance == null)
		{
			return false;
		}
		if ((Object)(object)ItemManager.Instance.GetItemByName("Coin") != (Object)null)
		{
			return (Object)(object)InventoryUI.Instance != (Object)null;
		}
		return false;
	}

	public static void Grant(int gold)
	{
		if (gold <= 0)
		{
			return;
		}
		if (ItemManager.Instance == null || (Object)(object)InventoryUI.Instance == (Object)null)
		{
			BloodPlugin.Log.LogWarning((object)("Shrine of Blood could not pay " + gold + " coins: inventory is missing."));
			return;
		}
		InventoryItem itemByName = ItemManager.Instance.GetItemByName("Coin");
		if ((Object)(object)itemByName == (Object)null)
		{
			if (!missingCoin)
			{
				missingCoin = true;
				BloodPlugin.Log.LogWarning((object)"Shrine of Blood: no Coin item, so the payout was not given.");
			}
			return;
		}
		int num = gold;
		int num2 = 0;
		while (num > 0 && num2++ < 6)
		{
			InventoryItem val = ScriptableObject.CreateInstance<InventoryItem>();
			val.Copy(itemByName, num);
			int num3 = InventoryUI.Instance.AddItemToInventory(val);
			Object.Destroy((Object)(object)val);
			if (num3 <= 0)
			{
				break;
			}
			if (num3 >= num)
			{
				BloodPlugin.Log.LogWarning((object)("Shrine of Blood paid " + (gold - num) + " of " + gold + " coins. The inventory is full."));
				break;
			}
			num = num3;
		}
	}
}
[BepInPlugin("evan.muck.shrineofblood", "Shrine of Blood", "0.1.0")]
public class BloodPlugin : BaseUnityPlugin
{
	public const string Guid = "evan.muck.shrineofblood";

	public const string Name = "Shrine of Blood";

	public const string Version = "0.1.0";

	internal static ManualLogSource Log;

	internal static BloodConfig Cfg;

	internal static PayLedger Pays = new PayLedger();

	internal static EchoGate Echo = new EchoGate();

	private void Awake()
	{
		//IL_0034: Unknown result type (might be due to invalid IL or missing references)
		//IL_003a: Expected O, but got Unknown
		Log = ((BaseUnityPlugin)this).Logger;
		Cfg = new BloodConfig(((BaseUnityPlugin)this).Config);
		Pays = new PayLedger();
		Echo = new EchoGate();
		Harmony harmony = new Harmony("evan.muck.shrineofblood");
		if (!TryPatch(harmony, typeof(StructureSpawnerStartPatch)))
		{
			Log.LogError((object)"Shrine of Blood 0.1.0 could not hook world generation. No shrines will appear.");
			return;
		}
		if (!TryPatch(harmony, typeof(ChatLinePatch)))
		{
			Log.LogWarning((object)"Shrine of Blood could not hook chat. Shrines will appear, but a use will not sync.");
		}
		Log.LogInfo((object)("Shrine of Blood 0.1.0 loaded. Config " + Cfg.Code() + "."));
	}

	private static bool TryPatch(Harmony harmony, Type patchClass)
	{
		try
		{
			harmony.CreateClassProcessor(patchClass).Patch();
			return true;
		}
		catch (Exception ex)
		{
			Log.LogError((object)("Shrine of Blood failed to patch " + patchClass.Name + ": " + ex));
			return false;
		}
	}
}
public class BloodShrine : MonoBehaviour, Interactable, SharedObject
{
	private static readonly Color Spent = new Color(0.25f, 0.16f, 0.16f);

	private int id;

	private ShrineUses shown;

	private ShrineUses host;

	private float pendingUntil;

	private Material[] materials;

	public static void Build(int shrineId, Vector3 ground, int uses)
	{
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		//IL_000b: Expected O, but got Unknown
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_003c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0055: Unknown result type (might be due to invalid IL or missing references)
		//IL_0064: Unknown result type (might be due to invalid IL or missing references)
		//IL_0069: Unknown result type (might be due to invalid IL or missing references)
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_009f: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
		GameObject val = new GameObject("ShrineOfBlood");
		val.transform.position = ground;
		val.layer = 15;
		BoxCollider obj = val.AddComponent<BoxCollider>();
		((Collider)obj).isTrigger = true;
		obj.size = new Vector3(2.2f, 2.2f, 2.2f);
		obj.center = new Vector3(0f, 1f, 0f);
		GameObject val2 = new GameObject("ShrineOfBlood_solid")
		{
			layer = 10
		};
		val2.transform.SetParent(val.transform, false);
		BoxCollider obj2 = val2.AddComponent<BoxCollider>();
		((Collider)obj2).isTrigger = false;
		obj2.size = new Vector3(1.7f, 1.7f, 1.7f);
		obj2.center = new Vector3(0f, 0.85f, 0f);
		BloodShrine bloodShrine = val.AddComponent<BloodShrine>();
		bloodShrine.SetId(shrineId);
		bloodShrine.shown = new ShrineUses(uses);
		bloodShrine.host = new ShrineUses(uses);
		bloodShrine.materials = ShrineMesh.Attach(val.transform);
		BloodShrines.Add(shrineId, bloodShrine);
		ResourceManager.Instance.AddObject(shrineId, val);
	}

	public void Interact()
	{
		if (shown == null || shown.Spent || Time.time < pendingUntil)
		{
			return;
		}
		PlayerStatus instance = PlayerStatus.Instance;
		if ((Object)(object)instance == (Object)null || instance.IsPlayerDead())
		{
			return;
		}
		BloodConfig cfg = BloodPlugin.Cfg;
		int maxUses = cfg?.MaxUses.Value ?? 3;
		float firstPercent = ((cfg != null) ? ((float)cfg.FirstCostPercent.Value) : 50f);
		float goldMultiplier = cfg?.GoldMultiplier.Value ?? 0.5f;
		BloodQuote quote = BloodMath.Quote(instance.hp, shown.UseIndex, maxUses, firstPercent, goldMultiplier);
		if (!quote.Ok)
		{
			Say(BloodText.TooHurt);
			return;
		}
		if (quote.Gold > 0 && !BloodPayout.CanGrant())
		{
			Say(BloodText.NoCoins);
			return;
		}
		pendingUntil = Time.time + 3f;
		if (LocalClient.serverOwner)
		{
			Commit(MyId(), quote);
		}
		else
		{
			ClientSend.SendChatMessage(BloodWire.Ask(id, quote.UseIndex, BloodWire.Milli(quote.Hp)));
		}
	}

	public void LocalExecute()
	{
	}

	public void ServerExecute(int fromClient = -1)
	{
	}

	public void AllExecute()
	{
	}

	public void RemoveObject()
	{
		Object.Destroy((Object)(object)((Component)this).gameObject);
	}

	public string GetName()
	{
		if (shown == null)
		{
			return BloodText.Prompt(spent: false, BloodMath.PercentAt(50f, 0));
		}
		BloodConfig cfg = BloodPlugin.Cfg;
		float firstPercent = ((cfg != null) ? ((float)cfg.FirstCostPercent.Value) : 50f);
		return BloodText.Prompt(shown.Spent, BloodMath.PercentAt(firstPercent, shown.UseIndex));
	}

	public bool IsStarted()
	{
		if (shown != null)
		{
			return shown.Spent;
		}
		return false;
	}

	public void SetId(int newId)
	{
		id = newId;
	}

	public int GetId()
	{
		return id;
	}

	public void AcceptAsk(int payerId, int useIndex, int hpMilli)
	{
		if (host == null || payerId < 0)
		{
			return;
		}
		BloodConfig cfg = BloodPlugin.Cfg;
		int maxUses = cfg?.MaxUses.Value ?? 3;
		float firstPercent = ((cfg != null) ? ((float)cfg.FirstCostPercent.Value) : 50f);
		float goldMultiplier = cfg?.GoldMultiplier.Value ?? 0.5f;
		if (!BloodHost.TryAccept(host.UseIndex, useIndex, BloodWire.FromMilli(hpMilli), maxUses, firstPercent, goldMultiplier, out var quote))
		{
			if (cfg != null && cfg.VerboseLogging.Value)
			{
				BloodPlugin.Log.LogInfo((object)("Shrine " + id + " refused use " + useIndex + " from client " + payerId + "."));
			}
		}
		else
		{
			Commit(payerId, quote);
		}
	}

	internal void ApplyPay(PayMessage pay)
	{
		pendingUntil = 0f;
		if (shown != null && pay.UseIndex >= shown.UseIndex)
		{
			while (shown.UseIndex <= pay.UseIndex && shown.TrySpend())
			{
			}
			if (shown.Spent)
			{
				Grey();
			}
		}
		if (pay.PayerId != MyId())
		{
			return;
		}
		PlayerStatus instance = PlayerStatus.Instance;
		if (!((Object)(object)instance == (Object)null) && !instance.IsPlayerDead() && instance.hp > 0f)
		{
			float num = BloodMath.Apply(instance.hp, BloodWire.FromMilli(pay.CostMilli));
			if (num < 1f)
			{
				num = 1f;
			}
			instance.hp = num;
			BloodPayout.Grant(pay.Gold);
		}
	}

	private void Commit(int payerId, BloodQuote quote)
	{
		if (host != null && host.UseIndex == quote.UseIndex && host.TrySpend())
		{
			string text = BloodText.Activation(PlayerNames.Of(payerId), quote.Cost, quote.Gold, host.Left);
			string text2 = BloodWire.Pay(id, payerId, quote.UseIndex, BloodWire.Milli(quote.Cost), quote.Gold);
			ServerSend.SendChatMessage(-1, "", text2);
			ServerSend.SendChatMessage(-1, "", text);
			Say(text2);
			Say(text);
			BloodPlugin.Log.LogInfo((object)text);
		}
	}

	private void OnDestroy()
	{
		BloodShrines.Remove(id, this);
		if (materials == null)
		{
			return;
		}
		for (int i = 0; i < materials.Length; i++)
		{
			if ((Object)(object)materials[i] != (Object)null)
			{
				Object.Destroy((Object)(object)materials[i]);
			}
		}
	}

	private void Grey()
	{
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		if (materials == null)
		{
			return;
		}
		for (int i = 0; i < materials.Length; i++)
		{
			if ((Object)(object)materials[i] != (Object)null)
			{
				materials[i].color = Spent;
			}
		}
	}

	private static int MyId()
	{
		if (!((Object)(object)LocalClient.instance != (Object)null))
		{
			return -1;
		}
		return LocalClient.instance.myId;
	}

	private static void Say(string message)
	{
		ChatBox instance = ChatBox.Instance;
		if ((Object)(object)instance != (Object)null)
		{
			instance.AppendMessage(-1, message, "");
		}
	}
}
internal static class BloodShrines
{
	private static readonly Dictionary<int, BloodShrine> All = new Dictionary<int, BloodShrine>();

	public static void Add(int id, BloodShrine shrine)
	{
		All[id] = shrine;
	}

	public static void Remove(int id, BloodShrine shrine)
	{
		if (All.TryGetValue(id, out var value) && value == shrine)
		{
			All.Remove(id);
		}
	}

	public static bool TryGet(int id, out BloodShrine shrine)
	{
		return All.TryGetValue(id, out shrine);
	}

	public static void RetireAll()
	{
		List<BloodShrine> list = new List<BloodShrine>(All.Values);
		All.Clear();
		for (int i = 0; i < list.Count; i++)
		{
			if ((Object)(object)list[i] != (Object)null)
			{
				Object.Destroy((Object)(object)((Component)list[i]).gameObject);
			}
		}
	}
}
internal static class BloodText
{
	public const int ChatMax = 120;

	public const string Head = "Shrine of Blood: ";

	private static readonly string[] Reserved = new string[5] { "~sv", "~wr", "~bq", "~gs", "~cd" };

	public static string TooHurt => "Not enough health.";

	public static string NoCoins => "The shrine has no coins to give.";

	public static string Prompt(bool spent, float fraction)
	{
		if (spent)
		{
			return "Shrine of Blood (spent)";
		}
		return "Shrine of Blood (" + PercentLabel(fraction) + "% HP)";
	}

	public static string Player(string name)
	{
		if (string.IsNullOrWhiteSpace(name))
		{
			return "Someone";
		}
		name = name.Replace('\r', ' ').Replace('\n', ' ').Trim();
		if (name.Length > 40)
		{
			name = name.Substring(0, 40).Trim();
		}
		if (name.Length != 0)
		{
			return name;
		}
		return "Someone";
	}

	public static string Activation(string playerName, float hpSpent, int gold, int usesLeftHere)
	{
		string text = Player(playerName);
		string text2 = ((gold == 1) ? "coin" : "coins");
		string text3 = ((usesLeftHere <= 0) ? "It is spent." : ((usesLeftHere == 1) ? "1 use left here." : (usesLeftHere + " uses left here.")));
		return Fit("Shrine of Blood: " + text + " paid " + Hp(hpSpent) + " HP for " + gold.ToString(CultureInfo.InvariantCulture) + " " + text2 + ". " + text3);
	}

	public static string Spot(float standX, float standY, float standZ, float aimX, float aimY, float aimZ)
	{
		return string.Format(CultureInfo.InvariantCulture, "Shrine of Blood spot {0:0.0} {1:0.0} {2:0.0} {3:0.0} {4:0.0} {5:0.0}", standX, standY, standZ, aimX, aimY, aimZ);
	}

	public static string Placed(int count, int seed)
	{
		return "Placed " + count.ToString(CultureInfo.InvariantCulture) + " Shrines of Blood (seed " + seed.ToString(CultureInfo.InvariantCulture) + ")";
	}

	public static bool IsActivation(string message)
	{
		if (!string.IsNullOrEmpty(message))
		{
			return message.StartsWith("Shrine of Blood: ");
		}
		return false;
	}

	public static bool UsesReservedPrefix(string message)
	{
		if (string.IsNullOrEmpty(message))
		{
			return false;
		}
		for (int i = 0; i < Reserved.Length; i++)
		{
			if (message.StartsWith(Reserved[i]))
			{
				return true;
			}
		}
		return false;
	}

	public static string PercentLabel(float fraction)
	{
		if (float.IsNaN(fraction) || fraction < 0f)
		{
			return "0";
		}
		double num = Math.Round((double)fraction * 100.0, 1, MidpointRounding.AwayFromZero);
		if (Math.Abs(num - Math.Round(num)) < 0.05)
		{
			return ((int)Math.Round(num)).ToString(CultureInfo.InvariantCulture);
		}
		return num.ToString("0.0", CultureInfo.InvariantCulture);
	}

	public static string Hp(float value)
	{
		if (float.IsNaN(value) || value < 0f)
		{
			return "0";
		}
		double num = Math.Round(value, 1, MidpointRounding.AwayFromZero);
		if (Math.Abs(num - Math.Round(num)) < 0.05)
		{
			return ((int)Math.Round(num)).ToString(CultureInfo.InvariantCulture);
		}
		return num.ToString("0.0", CultureInfo.InvariantCulture);
	}

	public static string Fit(string line)
	{
		if (string.IsNullOrEmpty(line))
		{
			return "";
		}
		if (line.Length <= 120)
		{
			return line;
		}
		return line.Substring(0, 120);
	}
}
internal struct PayMessage
{
	public int ShrineId;

	public int PayerId;

	public int UseIndex;

	public int CostMilli;

	public int Gold;
}
internal static class BloodWire
{
	public const string Token = "~sb1";

	public const string Family = "~sb";

	public static bool IsHidden(string message)
	{
		if (!string.IsNullOrEmpty(message))
		{
			return message.StartsWith("~sb");
		}
		return false;
	}

	public static int Milli(float hp)
	{
		if (float.IsNaN(hp) || float.IsInfinity(hp) || hp <= 0f)
		{
			return 0;
		}
		double num = Math.Round((double)hp * 1000.0, MidpointRounding.AwayFromZero);
		if (num > 2147483647.0)
		{
			return int.MaxValue;
		}
		return (int)num;
	}

	public static float FromMilli(int milli)
	{
		if (milli <= 0)
		{
			return 0f;
		}
		return (float)milli / 1000f;
	}

	public static string Ask(int shrineId, int useIndex, int hpMilli)
	{
		return string.Format(CultureInfo.InvariantCulture, "{0} a {1} {2} {3}", "~sb1", shrineId, useIndex, hpMilli);
	}

	public static string Pay(int shrineId, int payerId, int useIndex, int costMilli, int gold)
	{
		return string.Format(CultureInfo.InvariantCulture, "{0} p {1} {2} {3} {4} {5}", "~sb1", shrineId, payerId, useIndex, costMilli, gold);
	}

	public static bool TryParseAsk(string message, out int shrineId, out int useIndex, out int hpMilli)
	{
		shrineId = 0;
		useIndex = 0;
		hpMilli = 0;
		string[] array = Split(message);
		if (array == null || array.Length != 5 || array[0] != "~sb1" || array[1] != "a")
		{
			return false;
		}
		if (!ParseId(array[2], out shrineId) || !ParseUse(array[3], out useIndex))
		{
			return false;
		}
		if (!int.TryParse(array[4], NumberStyles.None, CultureInfo.InvariantCulture, out hpMilli))
		{
			return false;
		}
		if (hpMilli <= 0 || hpMilli > 100000000)
		{
			return false;
		}
		return true;
	}

	public static bool TryParsePay(string message, out PayMessage pay)
	{
		pay = default(PayMessage);
		string[] array = Split(message);
		if (array == null || array.Length != 7 || array[0] != "~sb1" || array[1] != "p")
		{
			return false;
		}
		if (!ParseId(array[2], out pay.ShrineId) || !ParseId(array[3], out pay.PayerId) || !ParseUse(array[4], out pay.UseIndex))
		{
			return false;
		}
		if (!int.TryParse(array[5], NumberStyles.None, CultureInfo.InvariantCulture, out pay.CostMilli))
		{
			return false;
		}
		if (!int.TryParse(array[6], NumberStyles.None, CultureInfo.InvariantCulture, out pay.Gold))
		{
			return false;
		}
		if (pay.CostMilli <= 0 || pay.CostMilli > 100000000)
		{
			return false;
		}
		if (pay.Gold < 0 || pay.Gold > 1000000)
		{
			return false;
		}
		return true;
	}

	private static bool ParseId(string text, out int id)
	{
		if (int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out id))
		{
			return id >= 0;
		}
		return false;
	}

	private static bool ParseUse(string text, out int useIndex)
	{
		if (int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out useIndex) && useIndex >= 0)
		{
			return useIndex < 64;
		}
		return false;
	}

	private static string[] Split(string message)
	{
		if (string.IsNullOrEmpty(message))
		{
			return null;
		}
		return message.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
	}
}
internal static class BloodWorld
{
	private static ResourceManager placedFor;

	private static int placedSeed;

	private static bool warnedMissingManager;

	private static readonly List<(double X, double Z)> placed = new List<(double, double)>();

	public static void PlaceOnce(StructureSpawner spawner)
	{
		//IL_013e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0143: 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)
		//IL_0166: Unknown result type (might be due to invalid IL or missing references)
		//IL_0171: Unknown result type (might be due to invalid IL or missing references)
		//IL_0177: Invalid comparison between Unknown and I4
		//IL_0182: Unknown result type (might be due to invalid IL or missing references)
		//IL_0188: Invalid comparison between Unknown and I4
		//IL_0192: Unknown result type (might be due to invalid IL or missing references)
		//IL_0198: Invalid comparison between Unknown and I4
		//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
		//IL_01d2: Unknown result type (might be due to invalid IL or missing references)
		//IL_01de: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ea: Unknown result type (might be due to invalid IL or missing references)
		ResourceManager instance = ResourceManager.Instance;
		int seed = GameManager.GetSeed();
		bool num = instance != null;
		switch (WorldGate.Decide(num, num && (Object)(object)placedFor == (Object)(object)instance, placedSeed == seed))
		{
		case PlaceKind.MissingManager:
			if (!warnedMissingManager)
			{
				warnedMissingManager = true;
				BloodPlugin.Log.LogWarning((object)"Shrine of Blood skipped placement: ResourceManager.Instance is null.");
			}
			break;
		case PlaceKind.AlreadyPlaced:
			break;
		case PlaceKind.NewWorld:
		case PlaceKind.NewMap:
		{
			BloodShrines.RetireAll();
			if (BloodPlugin.Pays != null)
			{
				BloodPlugin.Pays.Reset();
			}
			if (BloodPlugin.Echo != null)
			{
				BloodPlugin.Echo.Reset();
			}
			placedFor = instance;
			placedSeed = seed;
			placed.Clear();
			warnedMissingManager = false;
			float halfExtent = Placement.HalfExtent(MapGenerator.mapChunkSize, spawner.worldScale);
			BloodConfig cfg = BloodPlugin.Cfg;
			int num2 = Placement.ClampCount(cfg.Count.Value);
			float value = cfg.MinSpacing.Value;
			int value2 = cfg.MaxUses.Value;
			int num3 = num2 * 8;
			int num4 = 0;
			RaycastHit val = default(RaycastHit);
			for (int i = 0; i < num3; i++)
			{
				if (num4 >= num2)
				{
					break;
				}
				(double, double) item = Placement.Candidate(seed, i, halfExtent);
				if (Placement.FarEnough(placed, item.Item1, item.Item2, value) && Physics.Raycast(new Vector3((float)item.Item1, 200f, (float)item.Item2), Vector3.down, ref val, 500f, LayerMask.op_Implicit(spawner.whatIsTerrain)))
				{
					float y = ((RaycastHit)(ref val)).point.y;
					bool hit = (int)WorldUtility.WorldHeightToBiome(y) == 2;
					bool below = (int)WorldUtility.WorldHeightToBiome(y - 0.5f) == 2;
					bool above = (int)WorldUtility.WorldHeightToBiome(y + 0.5f) == 2;
					if (Placement.InsideGrass(hit, below, above))
					{
						BloodShrine.Build(ShrineIds.For(i), ((RaycastHit)(ref val)).point, value2);
						placed.Add(item);
						num4++;
						Placement.StandAndAim(((RaycastHit)(ref val)).point.x, ((RaycastHit)(ref val)).point.y, ((RaycastHit)(ref val)).point.z, out var sx, out var sy, out var sz, out var ax, out var ay, out var az);
						BloodPlugin.Log.LogInfo((object)BloodText.Spot(sx, sy, sz, ax, ay, az));
					}
				}
			}
			BloodPlugin.Log.LogInfo((object)BloodText.Placed(num4, seed));
			BloodPlugin.Log.LogInfo((object)("Shrine of Blood config " + cfg.Code() + "."));
			break;
		}
		}
	}
}
[HarmonyPatch(typeof(ChatBox), "AppendMessage", new Type[]
{
	typeof(int),
	typeof(string),
	typeof(string)
})]
internal static class ChatLinePatch
{
	private static bool Prefix(int fromUser, string message)
	{
		bool flag = BloodWire.IsHidden(message);
		try
		{
			if (flag)
			{
				PayMessage pay;
				BloodShrine shrine2;
				if (fromUser >= 0 && LocalClient.serverOwner && BloodWire.TryParseAsk(message, out var shrineId, out var useIndex, out var hpMilli))
				{
					if (BloodShrines.TryGet(shrineId, out var shrine))
					{
						shrine.AcceptAsk(fromUser, useIndex, hpMilli);
					}
				}
				else if (fromUser < 0 && BloodWire.TryParsePay(message, out pay) && BloodPlugin.Pays != null && BloodPlugin.Pays.Claim(pay.ShrineId, pay.UseIndex) && BloodShrines.TryGet(pay.ShrineId, out shrine2))
				{
					shrine2.ApplyPay(pay);
				}
				return false;
			}
			if (BloodText.IsActivation(message) && BloodPlugin.Echo != null)
			{
				return BloodPlugin.Echo.Allow(message, Time.time);
			}
		}
		catch (Exception ex)
		{
			if (BloodPlugin.Log != null)
			{
				BloodPlugin.Log.LogError((object)("Shrine of Blood chat failed: " + ex));
			}
		}
		return !flag;
	}
}
internal static class ConfigStamp
{
	public static string Code(int count, int spacingCm, int firstPercent, int goldThousand, int maxUses)
	{
		uint h = 2166136261u;
		h = Mix(h, count);
		h = Mix(h, spacingCm);
		h = Mix(h, firstPercent);
		h = Mix(h, goldThousand);
		return Mix(h, maxUses).ToString("X8");
	}

	private static uint Mix(uint h, int value)
	{
		h ^= (uint)value;
		return h * 16777619;
	}
}
internal sealed class EchoGate
{
	private string line;

	private float at = float.NaN;

	public bool Allow(string message, float now, float window = 2f)
	{
		if (!float.IsNaN(at) && message == line && now >= at && now - at < window)
		{
			return false;
		}
		line = message;
		at = now;
		return true;
	}

	public void Reset()
	{
		line = null;
		at = float.NaN;
	}
}
internal static class Hash01
{
	public static ulong SplitMix64(ulong x)
	{
		long num = (long)x + -7046029254386353131L;
		long num2 = (num ^ (num >>> 30)) * -4658895280553007687L;
		long num3 = (num2 ^ (num2 >>> 27)) * -7723592293110705685L;
		return (ulong)(num3 ^ (num3 >>> 31));
	}

	public static double Of(int a, int b, int c)
	{
		return ToUnit(SplitMix64(SplitMix64(Key(a, b)) ^ (uint)c));
	}

	private static ulong Key(int a, int b)
	{
		return ((ulong)(uint)a << 32) | (uint)b;
	}

	private static double ToUnit(ulong h)
	{
		return (double)(h >> 11) * 1.1102230246251565E-16;
	}
}
internal sealed class ObjPart
{
	public readonly string Material;

	public readonly List<float> Positions = new List<float>();

	public int Triangles => Positions.Count / 9;

	public ObjPart(string material)
	{
		Material = material;
	}

	public void Add(List<float> verts, int a, int b, int c)
	{
		Copy(verts, a);
		Copy(verts, b);
		Copy(verts, c);
	}

	private void Copy(List<float> verts, int index)
	{
		int num = index * 3;
		Positions.Add(verts[num]);
		Positions.Add(verts[num + 1]);
		Positions.Add(verts[num + 2]);
	}
}
internal sealed class ObjModel
{
	public readonly List<ObjPart> Parts;

	public int Triangles
	{
		get
		{
			int num = 0;
			for (int i = 0; i < Parts.Count; i++)
			{
				num += Parts[i].Triangles;
			}
			return num;
		}
	}

	public ObjModel(List<ObjPart> parts)
	{
		Parts = parts;
	}

	public void Bounds(out float minX, out float minY, out float minZ, out float maxX, out float maxY, out float maxZ)
	{
		minX = (minY = (minZ = float.PositiveInfinity));
		maxX = (maxY = (maxZ = float.NegativeInfinity));
		for (int i = 0; i < Parts.Count; i++)
		{
			List<float> positions = Parts[i].Positions;
			for (int j = 0; j < positions.Count; j += 3)
			{
				float num = positions[j];
				float num2 = positions[j + 1];
				float num3 = positions[j + 2];
				if (num < minX)
				{
					minX = num;
				}
				if (num2 < minY)
				{
					minY = num2;
				}
				if (num3 < minZ)
				{
					minZ = num3;
				}
				if (num > maxX)
				{
					maxX = num;
				}
				if (num2 > maxY)
				{
					maxY = num2;
				}
				if (num3 > maxZ)
				{
					maxZ = num3;
				}
			}
		}
	}

	public static ObjModel Parse(string text)
	{
		List<float> list = new List<float>();
		List<ObjPart> list2 = new List<ObjPart>();
		ObjPart objPart = null;
		string text2 = "stone";
		if (string.IsNullOrEmpty(text))
		{
			return new ObjModel(list2);
		}
		string[] array = text.Split(new char[2] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
		for (int i = 0; i < array.Length; i++)
		{
			string text3 = array[i].Trim();
			if (text3.Length == 0 || text3[0] == '#')
			{
				continue;
			}
			if (text3.StartsWith("usemtl "))
			{
				text2 = text3.Substring(7).Trim();
				objPart = null;
			}
			else if (text3.StartsWith("v "))
			{
				string[] array2 = text3.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
				list.Add(float.Parse(array2[1], CultureInfo.InvariantCulture));
				list.Add(float.Parse(array2[2], CultureInfo.InvariantCulture));
				list.Add(float.Parse(array2[3], CultureInfo.InvariantCulture));
			}
			else
			{
				if (!text3.StartsWith("f "))
				{
					continue;
				}
				string[] array3 = text3.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
				List<int> list3 = new List<int>(array3.Length - 1);
				int count = list.Count / 3;
				for (int j = 1; j < array3.Length; j++)
				{
					list3.Add(VertexIndex(array3[j], count));
				}
				if (list3.Count >= 3)
				{
					if (objPart == null || objPart.Material != text2)
					{
						objPart = new ObjPart(text2);
						list2.Add(objPart);
					}
					for (int k = 1; k < list3.Count - 1; k++)
					{
						objPart.Add(list, list3[0], list3[k], list3[k + 1]);
					}
				}
			}
		}
		return new ObjModel(list2);
	}

	private static int VertexIndex(string corner, int count)
	{
		int num = int.Parse(corner.Split(new char[1] { '/' })[0], CultureInfo.InvariantCulture);
		if (num < 0)
		{
			return count + num;
		}
		return num - 1;
	}
}
internal sealed class PayLedger
{
	private readonly HashSet<long> seen = new HashSet<long>();

	public bool Claim(int shrineId, int useIndex)
	{
		long item = ((long)shrineId << 8) ^ (uint)useIndex;
		return seen.Add(item);
	}

	public void Reset()
	{
		seen.Clear();
	}
}
internal static class Placement
{
	public const int Salt = 1112294449;

	public const float BiomeMargin = 0.5f;

	public const int MaxCount = 16;

	public const float StandDistance = 2.5f;

	public const float Eye = 1f;

	public const float AimHeight = 0.55f;

	public static int ClampCount(int count)
	{
		if (count < 0)
		{
			return 0;
		}
		if (count > 16)
		{
			return 16;
		}
		return count;
	}

	public static float HalfExtent(int mapChunkSize, float worldScaleAfterBuffer)
	{
		return (float)mapChunkSize / 2f * worldScaleAfterBuffer;
	}

	public static (double X, double Z) Candidate(int seed, int attempt, float halfExtent)
	{
		double item = (Hash01.Of(seed, 1112294449, 2 * attempt) * 2.0 - 1.0) * (double)halfExtent;
		double item2 = (Hash01.Of(seed, 1112294449, 2 * attempt + 1) * 2.0 - 1.0) * (double)halfExtent;
		return (X: item, Z: item2);
	}

	public static bool FarEnough(IReadOnlyList<(double X, double Z)> placed, double x, double z, float minSpacing)
	{
		double num = (double)minSpacing * (double)minSpacing;
		for (int i = 0; i < placed.Count; i++)
		{
			double num2 = placed[i].X - x;
			double num3 = placed[i].Z - z;
			if (num2 * num2 + num3 * num3 < num)
			{
				return false;
			}
		}
		return true;
	}

	public static bool InsideGrass(bool hit, bool below, bool above)
	{
		return hit && below && above;
	}

	public static void StandAndAim(float x, float y, float z, out float sx, out float sy, out float sz, out float ax, out float ay, out float az)
	{
		sx = x + 2.5f;
		sy = y + 1f;
		sz = z;
		ax = x;
		ay = y + 0.55f;
		az = z;
	}
}
internal static class PlayerNames
{
	public static string Of(int clientId)
	{
		Dictionary<int, PlayerManager> players = GameManager.players;
		if (players != null && players.TryGetValue(clientId, out var value) && (Object)(object)value != (Object)null && !string.IsNullOrEmpty(value.username))
		{
			return value.username;
		}
		return "Someone";
	}
}
internal static class ShrineIds
{
	public const int Base = 1500000000;

	public const int Span = 1000000;

	public static int For(int attempt)
	{
		return 1500000000 + attempt;
	}

	public static bool IsOurs(int id)
	{
		if (id >= 1500000000)
		{
			return id < 1501000000;
		}
		return false;
	}
}
internal static class ShrineMesh
{
	public const string ResourceName = "MuckShrineOfBlood.shrine.obj";

	private static Mesh[] meshes;

	private static string[] materials;

	private static bool failed;

	private static Shader shader;

	private static bool shaderLogged;

	public static Material[] Attach(Transform parent)
	{
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_0047: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_005c: Unknown result type (might be due to invalid IL or missing references)
		//IL_007e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0085: Expected O, but got Unknown
		//IL_008e: Unknown result type (might be due to invalid IL or missing references)
		Ensure();
		if (failed || meshes == null)
		{
			return (Material[])(object)new Material[0];
		}
		Shader val = ShaderNow();
		Material[] array = (Material[])(object)new Material[meshes.Length];
		for (int i = 0; i < meshes.Length; i++)
		{
			GameObject val2 = new GameObject("ShrineOfBlood_" + materials[i])
			{
				layer = 10
			};
			val2.transform.SetParent(parent, false);
			val2.AddComponent<MeshFilter>().sharedMesh = meshes[i];
			MeshRenderer val3 = val2.AddComponent<MeshRenderer>();
			if (!((Object)(object)val == (Object)null))
			{
				Material val4 = new Material(val);
				val4.color = ShrinePalette.ColorOf(materials[i]);
				array[i] = val4;
				((Renderer)val3).sharedMaterial = val4;
			}
		}
		return array;
	}

	public static string ReadResource()
	{
		using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MuckShrineOfBlood.shrine.obj");
		if (stream == null)
		{
			return null;
		}
		using StreamReader streamReader = new StreamReader(stream);
		return streamReader.ReadToEnd();
	}

	private static void Ensure()
	{
		//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c9: Expected O, but got Unknown
		//IL_011e: Unknown result type (might be due to invalid IL or missing references)
		if (meshes != null || failed)
		{
			return;
		}
		string text = ReadResource();
		if (string.IsNullOrEmpty(text))
		{
			failed = true;
			BloodPlugin.Log.LogError((object)"Shrine of Blood mesh is missing from the assembly.");
			return;
		}
		ObjModel objModel = ObjModel.Parse(text);
		if (objModel.Triangles <= 0)
		{
			failed = true;
			BloodPlugin.Log.LogError((object)"Shrine of Blood mesh parsed no triangles.");
			return;
		}
		meshes = (Mesh[])(object)new Mesh[objModel.Parts.Count];
		materials = new string[objModel.Parts.Count];
		for (int i = 0; i < objModel.Parts.Count; i++)
		{
			ObjPart objPart = objModel.Parts[i];
			materials[i] = objPart.Material;
			Mesh val = new Mesh
			{
				name = "ShrineOfBlood_" + objPart.Material,
				hideFlags = (HideFlags)61
			};
			List<Vector3> list = new List<Vector3>(objPart.Positions.Count / 3);
			List<int> list2 = new List<int>(objPart.Triangles * 3);
			for (int j = 0; j < objPart.Positions.Count; j += 3)
			{
				list.Add(new Vector3(objPart.Positions[j], objPart.Positions[j + 1], objPart.Positions[j + 2]));
				list2.Add(list.Count - 1);
			}
			val.SetVertices(list);
			val.SetTriangles(list2, 0);
			val.RecalculateNormals();
			val.RecalculateBounds();
			meshes[i] = val;
		}
	}

	private static Shader ShaderNow()
	{
		if ((Object)(object)shader != (Object)null)
		{
			return shader;
		}
		shader = Shader.Find("Standard");
		if ((Object)(object)shader == (Object)null)
		{
			shader = Shader.Find("Diffuse");
		}
		if ((Object)(object)shader == (Object)null)
		{
			shader = Shader.Find("Legacy Shaders/Diffuse");
		}
		if ((Object)(object)shader == (Object)null && !shaderLogged)
		{
			shaderLogged = true;
			BloodPlugin.Log.LogWarning((object)"Shrine of Blood found no lit shader. The collider is still there.");
		}
		return shader;
	}
}
internal static class ShrinePalette
{
	private static readonly Dictionary<string, float[]> Colors = new Dictionary<string, float[]>
	{
		{
			"stone",
			new float[3] { 0.55f, 0.5f, 0.46f }
		},
		{
			"stone_dark",
			new float[3] { 0.28f, 0.24f, 0.22f }
		},
		{
			"blood",
			new float[3] { 0.62f, 0.07f, 0.08f }
		},
		{
			"gold",
			new float[3] { 0.95f, 0.72f, 0.22f }
		}
	};

	public static IEnumerable<string> Names => Colors.Keys;

	public static bool Knows(string material)
	{
		if (material != null)
		{
			return Colors.ContainsKey(material);
		}
		return false;
	}

	public static Color ColorOf(string material)
	{
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		//IL_001b: Unknown result type (might be due to invalid IL or missing references)
		if (material != null && Colors.TryGetValue(material, out var value))
		{
			return new Color(value[0], value[1], value[2]);
		}
		return new Color(0.5f, 0.5f, 0.5f);
	}
}
internal sealed class ShrineUses
{
	public int Max { get; }

	public int UseIndex { get; private set; }

	public int Left => Max - UseIndex;

	public bool Spent => UseIndex >= Max;

	public ShrineUses(int uses)
	{
		Max = ((uses >= 0) ? uses : 0);
	}

	public bool TrySpend()
	{
		if (Spent)
		{
			return false;
		}
		UseIndex++;
		return true;
	}
}
[HarmonyPatch(typeof(StructureSpawner), "Start")]
internal static class StructureSpawnerStartPatch
{
	private static void Postfix(StructureSpawner __instance)
	{
		try
		{
			BloodWorld.PlaceOnce(__instance);
		}
		catch (Exception ex)
		{
			BloodPlugin.Log.LogError((object)("Shrine of Blood placement failed: " + ex));
		}
	}
}
internal enum PlaceKind
{
	NewWorld,
	NewMap,
	AlreadyPlaced,
	MissingManager
}
internal static class WorldGate
{
	public static PlaceKind Decide(bool hasManager, bool sameManager, bool sameSeed)
	{
		if (!hasManager)
		{
			return PlaceKind.MissingManager;
		}
		if (sameManager && sameSeed)
		{
			return PlaceKind.AlreadyPlaced;
		}
		if (sameManager)
		{
			return PlaceKind.NewMap;
		}
		return PlaceKind.NewWorld;
	}
}
public static class MyPluginInfo
{
	public const string PLUGIN_GUID = "MuckShrineOfBlood";

	public const string PLUGIN_NAME = "Shrine of Blood";

	public const string PLUGIN_VERSION = "0.1.0";
}