Decompiled source of VikingStoryteller v0.1.0

plugins/VikingStoryteller.dll

Decompiled 17 hours ago
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Newtonsoft.Json;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace VikingStoryteller;

[BepInPlugin("JacobsValheim.VikingStoryteller", "Viking Storyteller", "0.1.0")]
public class Plugin : BaseUnityPlugin
{
	private class Connection
	{
		public string token;

		public float helloAt;

		public float lastHello = -999f;

		public float window;

		public int received;
	}

	private class Cached
	{
		public Entity entity;

		public float time;
	}

	public class Capture
	{
		public DeathReport report;

		public ZDO zdo;

		public long owner;
	}

	private const string Hello = "JVStory_Hello_v1";

	private const string Accept = "JVStory_Accept_v1";

	private const string Report = "JVStory_Death_v1";

	public static Plugin Instance;

	private static readonly FieldInfo LastHit = AccessTools.Field(typeof(Character), "m_lastHit");

	private readonly Dictionary<ZRpc, Connection> connections = new Dictionary<ZRpc, Connection>();

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

	private readonly Queue<DeathReport> outgoing = new Queue<DeathReport>();

	private readonly Queue<string> lines = new Queue<string>();

	private readonly HashSet<string> emitted = new HashSet<string>();

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

	private StoryModel model;

	private ConfigEntry<bool> send;

	private ConfigEntry<bool> collect;

	private Harmony harmony;

	private string world;

	private string token;

	private string directory;

	private ZRpc acceptedServer;

	private float nextTick;

	private float nextSave;

	private float nextFlush;

	private long accepted;

	private long rejected;

	private long dropped;

	private long failures;

	private bool hooksReady;

	private static readonly JsonSerializerSettings JsonSettings;

	private void Awake()
	{
		//IL_0064: Unknown result type (might be due to invalid IL or missing references)
		//IL_006e: Expected O, but got Unknown
		//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e2: Expected O, but got Unknown
		//IL_00e2: Expected O, but got Unknown
		//IL_0122: Unknown result type (might be due to invalid IL or missing references)
		//IL_0130: Expected O, but got Unknown
		Instance = this;
		send = ((BaseUnityPlugin)this).Config.Bind<bool>("Reporting", "Send gameplay death reports", true, "Send owner-observed deaths to a server running this mod. No chat, inventory, credentials, or external service traffic.");
		collect = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "Collect gameplay death reports", true, "On a server, accept connected owners' reports and write local story files. Does not grant clients admin powers.");
		directory = Path.Combine(Paths.ConfigPath, "VikingStoryteller");
		harmony = new Harmony("JacobsValheim.VikingStoryteller");
		try
		{
			Type[] array = new Type[2]
			{
				typeof(Character),
				typeof(Player)
			};
			foreach (Type type in array)
			{
				harmony.Patch((MethodBase)AccessTools.DeclaredMethod(type, "OnDeath", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeDeath", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterDeath", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
			harmony.Patch((MethodBase)AccessTools.Method(typeof(ZDOMan), "HandleDestroyedZDO", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeDestroyed", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			hooksReady = true;
		}
		catch (Exception ex)
		{
			harmony.UnpatchSelf();
			((BaseUnityPlugin)this).Logger.LogError((object)("Death observation disabled: " + ex.Message));
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)"Read-only gameplay reporter loaded. No admin commands, gameplay changes, automatic announcements, or external analytics.");
	}

	private void OnDestroy()
	{
		if ((Object)(object)Instance == (Object)(object)this)
		{
			Instance = null;
		}
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
		if (model != null)
		{
			Flush();
			Save();
		}
	}

	private static bool Server()
	{
		return (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();
	}

	private static string CurrentWorld()
	{
		return (WorldGenerator.instance == null || WorldGenerator.instance.m_world == null) ? null : WorldGenerator.instance.m_world.m_uid.ToString();
	}

	private void Update()
	{
		if (Time.realtimeSinceStartup < nextTick)
		{
			return;
		}
		nextTick = Time.realtimeSinceStartup + 0.5f;
		try
		{
			Tick();
		}
		catch (Exception ex)
		{
			failures++;
			if (failures <= 3)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Story observation failed: " + ex.Message));
			}
		}
	}

	private void Tick()
	{
		//IL_03c5: Unknown result type (might be due to invalid IL or missing references)
		//IL_03ca: Unknown result type (might be due to invalid IL or missing references)
		//IL_03e9: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)ZNet.instance == (Object)null || ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null)
		{
			return;
		}
		string text = CurrentWorld();
		if (text == null)
		{
			return;
		}
		if (world != text)
		{
			if (model != null)
			{
				Flush();
				Save();
			}
			world = text;
			token = null;
			acceptedServer = null;
			connections.Clear();
			outgoing.Clear();
			cache.Clear();
			roster.Clear();
			emitted.Clear();
			model = null;
		}
		bool flag = Server();
		float now = Time.realtimeSinceStartup;
		if (flag && collect.Value && model == null)
		{
			Directory.CreateDirectory(directory);
			StoryState storyState = null;
			string text2 = Path.Combine(directory, world + "-state.json");
			if (File.Exists(text2))
			{
				try
				{
					storyState = JsonConvert.DeserializeObject<StoryState>(File.ReadAllText(text2), JsonSettings);
					if (storyState == null || storyState.deaths == null || storyState.deathIds == null || storyState.links == null)
					{
						throw new Exception("Invalid saved story state");
					}
				}
				catch
				{
					File.Copy(text2, text2 + ".unreadable-" + Guid.NewGuid().ToString("N"));
					failures++;
					storyState = null;
				}
			}
			model = new StoryModel(world, storyState, WriteEvent);
		}
		List<ZNetPeer> peers = ZNet.instance.GetPeers();
		foreach (ZNetPeer item in peers)
		{
			if (item.m_rpc == null)
			{
				continue;
			}
			if (!connections.TryGetValue(item.m_rpc, out var value))
			{
				Connection connection = new Connection();
				connection.helloAt = now + 5f;
				value = connection;
				connections[item.m_rpc] = value;
				if (flag)
				{
					item.m_rpc.Register<int>("JVStory_Hello_v1", (Action<ZRpc, int>)ReceiveHello);
					item.m_rpc.Register<string>("JVStory_Death_v1", (Action<ZRpc, string>)ReceiveReport);
				}
				else
				{
					item.m_rpc.Register<string>("JVStory_Accept_v1", (Action<ZRpc, string>)ReceiveAccept);
				}
			}
			if (!flag && send.Value && item.IsReady() && item == ZNet.instance.GetServerPeer() && acceptedServer != item.m_rpc && now >= value.helloAt)
			{
				if (value.received < 3)
				{
					item.m_rpc.Invoke("JVStory_Hello_v1", new object[1] { 1 });
					value.received++;
				}
				value.helloAt = now + 15f;
			}
			if (!flag || !item.IsReady() || !(item.m_characterID != ZDOID.None))
			{
				continue;
			}
			ZDO zDO = ZDOMan.instance.GetZDO(item.m_characterID);
			if (zDO != null)
			{
				Entity entity = FromZdo(zDO);
				if (entity != null)
				{
					entity.player = true;
					entity.account = item.m_socket.GetHostName();
					entity.name = Clean(item.m_playerName);
					roster[entity.id] = entity;
				}
			}
		}
		ZRpc[] array = connections.Keys.Where((ZRpc rpc) => !peers.Any((ZNetPeer p) => p.m_rpc == rpc)).ToArray();
		foreach (ZRpc key in array)
		{
			connections.Remove(key);
		}
		if (!flag && (acceptedServer == null || !peers.Any((ZNetPeer p) => p.m_rpc == acceptedServer)))
		{
			acceptedServer = null;
			token = null;
			outgoing.Clear();
		}
		if (!flag && send.Value && acceptedServer != null && token != null)
		{
			for (int num2 = 0; num2 < 8; num2++)
			{
				if (outgoing.Count <= 0)
				{
					break;
				}
				DeathReport deathReport = outgoing.Dequeue();
				deathReport.token = token;
				if (StoryModel.Valid(deathReport, world, ZNet.instance.GetTimeSeconds()))
				{
					acceptedServer.Invoke("JVStory_Death_v1", new object[1] { JsonConvert.SerializeObject((object)deathReport) });
				}
				else
				{
					dropped++;
				}
			}
		}
		string[] array2 = (from p in cache
			where now - p.Value.time > 30f
			select p.Key).ToArray();
		foreach (string key2 in array2)
		{
			cache.Remove(key2);
		}
		if (roster.Count > 4096)
		{
			array2 = roster.Keys.Take(roster.Count - 4096).ToArray();
			foreach (string key2 in array2)
			{
				roster.Remove(key2);
			}
		}
		if (flag && model != null)
		{
			if (now >= nextFlush)
			{
				nextFlush = now + 2f;
				Flush();
			}
			if (now >= nextSave)
			{
				nextSave = now + 15f;
				Save();
			}
		}
	}

	private void ReceiveHello(ZRpc rpc, int version)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (Server() && collect.Value && model != null && version == 1 && val != null && connections.TryGetValue(rpc, out var value) && !(Time.realtimeSinceStartup - value.lastHello < 3f))
			{
				value.lastHello = Time.realtimeSinceStartup;
				if (value.token == null)
				{
					value.token = Guid.NewGuid().ToString("N");
				}
				rpc.Invoke("JVStory_Accept_v1", new object[1] { world + "|" + value.token });
			}
		}
		catch
		{
			failures++;
		}
	}

	private void ReceiveAccept(ZRpc rpc, string value)
	{
		try
		{
			if (!Server() && send.Value && value != null && value.Length <= 80 && ZNet.instance.GetServerPeer() != null && ZNet.instance.GetServerPeer().m_rpc == rpc)
			{
				string[] array = value.Split(new char[1] { '|' });
				if (array.Length == 2 && !(array[0] != world) && Guid.TryParse(array[1], out var _))
				{
					acceptedServer = rpc;
					token = array[1];
					((BaseUnityPlugin)this).Logger.LogInfo((object)"Gameplay reporting accepted by the connected server.");
				}
			}
		}
		catch
		{
			failures++;
		}
	}

	private static ZNetPeer Peer(ZRpc rpc)
	{
		return ((Object)(object)ZNet.instance == (Object)null) ? null : ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.IsReady() && p.m_rpc == rpc));
	}

	private void ReceiveReport(ZRpc rpc, string json)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (!Server() || !collect.Value || model == null || val == null || !connections.TryGetValue(rpc, out var value) || value.token == null)
			{
				rejected++;
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - value.window >= 10f)
			{
				value.window = realtimeSinceStartup;
				value.received = 0;
			}
			if (++value.received > 256 || json == null || json.Length > 4096)
			{
				rejected++;
				return;
			}
			DeathReport deathReport = JsonConvert.DeserializeObject<DeathReport>(json, JsonSettings);
			if (!StoryModel.Valid(deathReport, world, ZNet.instance.GetTimeSeconds()) || deathReport.token != value.token)
			{
				rejected++;
			}
			else
			{
				Record(deathReport, val.m_uid, val.m_socket.GetHostName(), "client_owner_report; native entity ownership checked; not anti-cheat proof");
			}
		}
		catch
		{
			rejected++;
		}
	}

	private void Record(DeathReport report, long sender, string reporter, string evidence)
	{
		Entity victim = Lookup(report.victim);
		Entity attacker = Lookup(report.attacker);
		if (model.Record(report, sender, reporter, victim, attacker, evidence, DateTime.UtcNow))
		{
			accepted++;
		}
		else
		{
			rejected++;
		}
	}

	public static string Clean(string text)
	{
		if (text == null)
		{
			return null;
		}
		text = text.Replace("\r", " ").Replace("\n", " ").Replace("<", "")
			.Replace(">", "");
		return (text.Length <= 128) ? text : text.Substring(0, 128);
	}

	private Entity FromZdo(ZDO z)
	{
		//IL_0186: Unknown result type (might be due to invalid IL or missing references)
		//IL_0192: Unknown result type (might be due to invalid IL or missing references)
		GameObject prefab = ZNetScene.instance.GetPrefab(z.GetPrefab());
		if ((Object)(object)prefab == (Object)null || (Object)(object)prefab.GetComponent<Character>() == (Object)null)
		{
			return null;
		}
		Entity entity = new Entity();
		entity.id = ((object)Unsafe.As<ZDOID, ZDOID>(ref z.m_uid)/*cast due to .constrained prefix*/).ToString();
		entity.prefab = ((Object)prefab).name;
		entity.owner = z.GetOwner();
		entity.player = (Object)(object)prefab.GetComponent<Player>() != (Object)null;
		entity.level = z.GetInt("level", 1);
		Entity entity2 = entity;
		if (entity2.player && roster.TryGetValue(entity2.id, out var value))
		{
			entity2.account = value.account;
			entity2.name = value.name;
		}
		if (entity2.player)
		{
			ZNetPeer val = ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_characterID == z.m_uid));
			if (val != null)
			{
				entity2.account = val.m_socket.GetHostName();
				entity2.name = Clean(val.m_playerName);
			}
			else if ((Object)(object)Player.m_localPlayer != (Object)null && ((Character)Player.m_localPlayer).GetZDOID() == z.m_uid)
			{
				entity2.account = "local-host";
				entity2.name = Clean(Player.m_localPlayer.GetPlayerName());
			}
		}
		return entity2;
	}

	private Entity Lookup(string id)
	{
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		if (!StoryModel.Id(id))
		{
			return null;
		}
		string[] array = id.Split(new char[1] { ':' });
		ZDO zDO = ZDOMan.instance.GetZDO(new ZDOID(long.Parse(array[0]), uint.Parse(array[1])));
		if (zDO != null)
		{
			return FromZdo(zDO);
		}
		if (cache.TryGetValue(id, out var value) && Time.realtimeSinceStartup - value.time <= 30f)
		{
			return value.entity;
		}
		Entity value2;
		return roster.TryGetValue(id, out value2) ? value2 : null;
	}

	private static void BeforeDestroyed(ZDOID __0)
	{
		//IL_0038: Unknown result type (might be due to invalid IL or missing references)
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !Server() || instance.model == null)
		{
			return;
		}
		try
		{
			ZDO zDO = ZDOMan.instance.GetZDO(__0);
			if (zDO == null)
			{
				return;
			}
			Entity entity = instance.FromZdo(zDO);
			if (entity != null)
			{
				if (instance.cache.Count >= 4096)
				{
					instance.dropped++;
					return;
				}
				instance.cache[entity.id] = new Cached
				{
					entity = entity,
					time = Time.realtimeSinceStartup
				};
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void BeforeDeath(Character __instance, out Capture __state)
	{
		//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
		//IL_0154: Unknown result type (might be due to invalid IL or missing references)
		//IL_0159: Unknown result type (might be due to invalid IL or missing references)
		//IL_0185: Unknown result type (might be due to invalid IL or missing references)
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !instance.hooksReady || (Object)(object)ZNet.instance == (Object)null || instance.world == null || (!Server() && (!instance.send.Value || instance.token == null)) || (Server() && (!instance.collect.Value || instance.model == null)))
		{
			return;
		}
		try
		{
			ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
			if ((Object)(object)component == (Object)null || !component.IsOwner() || component.GetZDO() == null || __instance.GetHealth() > 0f)
			{
				return;
			}
			ZDO zDO = component.GetZDO();
			object? value = LastHit.GetValue(__instance);
			HitData val = (HitData)((value is HitData) ? value : null);
			Vector3 position = ((Component)__instance).transform.position;
			__state = new Capture
			{
				zdo = zDO,
				owner = zDO.GetOwner(),
				report = new DeathReport
				{
					id = Guid.NewGuid().ToString(),
					world = instance.world,
					victim = ((object)Unsafe.As<ZDOID, ZDOID>(ref zDO.m_uid)/*cast due to .constrained prefix*/).ToString(),
					attacker = ((val == null || val.m_attacker == ZDOID.None) ? null : ((object)Unsafe.As<ZDOID, ZDOID>(ref val.m_attacker)/*cast due to .constrained prefix*/).ToString()),
					cause = ((val == null) ? "unknown" : ((object)val.m_hitType).ToString()),
					player = __instance.IsPlayer(),
					gameTime = ZNet.instance.GetTimeSeconds(),
					x = position.x,
					y = position.y,
					z = position.z
				}
			};
			if (Server())
			{
				Entity entity = instance.FromZdo(zDO);
				if (entity != null)
				{
					instance.cache[entity.id] = new Cached
					{
						entity = entity,
						time = Time.realtimeSinceStartup
					};
				}
			}
		}
		catch
		{
			instance.failures++;
			__state = null;
		}
	}

	private static void AfterDeath(Capture __state, bool __runOriginal)
	{
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || __state == null || !__runOriginal)
		{
			return;
		}
		try
		{
			DeathReport report = __state.report;
			if ((!report.player || __state.zdo.GetBool(ZDOVars.s_dead, false)) && instance.emitted.Add(report.victim))
			{
				if (instance.emitted.Count > 8192)
				{
					instance.emitted.Clear();
					instance.emitted.Add(report.victim);
				}
				if (Server())
				{
					instance.Record(report, __state.owner, "server", "server_owner_observation");
				}
				else if (instance.outgoing.Count < 256)
				{
					instance.outgoing.Enqueue(report);
				}
				else
				{
					instance.dropped++;
				}
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private void WriteEvent(StoryEvent e)
	{
		if (lines.Count >= 2048)
		{
			dropped++;
		}
		else
		{
			lines.Enqueue(JsonConvert.SerializeObject((object)e));
		}
	}

	private void Flush()
	{
		if (lines.Count == 0)
		{
			return;
		}
		try
		{
			string text = Path.Combine(directory, world + "-events.jsonl");
			if (File.Exists(text) && new FileInfo(text).Length >= 20971520)
			{
				File.Move(text, Path.Combine(directory, world + "-events-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff") + ".jsonl"));
				foreach (string item in (from x in Directory.GetFiles(directory, world + "-events-*.jsonl")
					orderby x descending
					select x).Skip(10))
				{
					File.Delete(item);
				}
			}
			File.AppendAllText(text, string.Join(Environment.NewLine, lines.ToArray()) + Environment.NewLine);
			lines.Clear();
		}
		catch
		{
			failures++;
		}
	}

	private void Atomic(string name, object data)
	{
		string text = Path.Combine(directory, name);
		string text2 = text + ".tmp";
		File.WriteAllText(text2, JsonConvert.SerializeObject(data, (Formatting)1));
		if (File.Exists(text))
		{
			File.Replace(text2, text, null);
		}
		else
		{
			File.Move(text2, text);
		}
	}

	private void Save()
	{
		if (model == null)
		{
			return;
		}
		try
		{
			Atomic(world + "-state.json", model.state);
			Atomic("status.json", new
			{
				utc = DateTime.UtcNow.ToString("o"),
				world = world,
				version = "0.1.0",
				hooksReady = hooksReady,
				accepted = accepted,
				rejected = rejected,
				dropped = dropped,
				failures = failures,
				reportingConnections = connections.Values.Count((Connection c) => c.token != null),
				queuedRecords = lines.Count,
				limitations = "Owner-reported deaths are not anti-cheat proof. Missing reporters, unknown prefabs, unresolved attackers, lag, mods replacing death handling and telemetry limits can leave gaps. No automatic announcements or roasts."
			});
		}
		catch
		{
			failures++;
		}
	}

	static Plugin()
	{
		//IL_0019: Unknown result type (might be due to invalid IL or missing references)
		//IL_001f: Expected O, but got Unknown
		JsonSerializerSettings val = new JsonSerializerSettings();
		val.TypeNameHandling = (TypeNameHandling)0;
		val.MaxDepth = 8;
		JsonSettings = val;
	}
}
public class DeathReport
{
	public int protocol = 1;

	public string token;

	public string id;

	public string world;

	public string victim;

	public string attacker;

	public string cause;

	public bool player;

	public double gameTime;

	public float x;

	public float y;

	public float z;
}
public class Entity
{
	public string id;

	public string prefab;

	public string account;

	public string name;

	public long owner;

	public bool player;

	public int level;
}
public class StoryEvent
{
	public string id;

	public string world;

	public string kind;

	public string observedUtc;

	public string evidence;

	public string reporter;

	public string sourceEvent;

	public string relatedEvent;

	public string beneficiary;

	public double gameTime;

	public string cause;

	public Entity victim;

	public Entity attacker;

	public float x;

	public float y;

	public float z;
}
public class StoryState
{
	public string world;

	public Dictionary<string, string> deathIds = new Dictionary<string, string>();

	public List<StoryEvent> deaths = new List<StoryEvent>();

	public HashSet<string> links = new HashSet<string>();
}
public class StoryModel
{
	public StoryState state;

	private readonly Action<StoryEvent> output;

	public StoryModel(string world, StoryState saved, Action<StoryEvent> output)
	{
		state = ((saved != null && saved.world == world) ? saved : new StoryState
		{
			world = world
		});
		this.output = output;
	}

	public static bool Id(string value)
	{
		if (string.IsNullOrEmpty(value) || value.Length > 40)
		{
			return false;
		}
		string[] array = value.Split(new char[1] { ':' });
		long result;
		uint result2;
		return array.Length == 2 && long.TryParse(array[0], out result) && uint.TryParse(array[1], out result2) && result != 0 && result2 != 0;
	}

	public static bool Valid(DeathReport report, string world, double now)
	{
		Guid result;
		return report != null && report.protocol == 1 && report.world == world && Guid.TryParse(report.id, out result) && Id(report.victim) && (report.attacker == null || Id(report.attacker)) && report.cause != null && report.cause.Length <= 64 && !double.IsNaN(report.gameTime) && !double.IsInfinity(report.gameTime) && Math.Abs(now - report.gameTime) <= 120.0 && !float.IsNaN(report.x + report.y + report.z) && !float.IsInfinity(report.x + report.y + report.z) && Math.Abs(report.x) <= 12000f && Math.Abs(report.z) <= 12000f && report.y >= -200f && report.y <= 10000f;
	}

	public static bool Authorized(DeathReport report, long sender, Entity victim)
	{
		return victim != null && victim.id == report.victim && victim.owner == sender && victim.player == report.player && (!report.player || !string.IsNullOrEmpty(victim.account));
	}

	public bool Record(DeathReport report, long sender, string reporter, Entity victim, Entity attacker, string evidence, DateTime now)
	{
		if (!Authorized(report, sender, victim) || state.deathIds.ContainsKey(report.victim))
		{
			return false;
		}
		if (attacker != null && attacker.id != report.attacker)
		{
			attacker = null;
		}
		StoryEvent e = new StoryEvent
		{
			id = report.id,
			world = state.world,
			kind = (report.player ? "player_death" : "creature_death"),
			observedUtc = now.ToString("o"),
			gameTime = report.gameTime,
			victim = victim,
			attacker = attacker,
			cause = report.cause,
			reporter = reporter,
			evidence = evidence,
			x = report.x,
			y = report.y,
			z = report.z
		};
		state.deathIds[report.victim] = e.id;
		state.deaths.Add(e);
		output(e);
		StoryEvent[] array = state.deaths.Where((StoryEvent d) => d.kind == "player_death" && d.attacker != null && !d.attacker.player && ((e.kind == "player_death") ? (d.id == e.id) : (d.attacker.id == e.victim.id))).ToArray();
		foreach (StoryEvent playerDeath in array)
		{
			List<StoryEvent> deaths = state.deaths;
			Func<StoryEvent, bool> predicate = (StoryEvent d) => d.kind == "creature_death" && (e.kind != "creature_death" || d.id == e.id) && d.victim.id == playerDeath.attacker.id && d.attacker != null && d.attacker.player && !string.IsNullOrEmpty(d.attacker.account) && d.gameTime >= playerDeath.gameTime;
			StoryEvent[] array2 = deaths.Where(predicate).ToArray();
			foreach (StoryEvent storyEvent in array2)
			{
				string item = playerDeath.id + "/" + storyEvent.id;
				if (state.links.Add(item))
				{
					output(new StoryEvent
					{
						id = Guid.NewGuid().ToString(),
						world = state.world,
						kind = ((storyEvent.attacker.account == playerDeath.victim.account) ? "revenge" : "avenged_by_teammate"),
						observedUtc = now.ToString("o"),
						gameTime = storyEvent.gameTime,
						victim = storyEvent.victim,
						attacker = storyEvent.attacker,
						beneficiary = playerDeath.victim.account,
						sourceEvent = storyEvent.id,
						relatedEvent = playerDeath.id,
						evidence = "matching_world_and_entity_ids_from_death_reports; client reports are not anti-cheat proof"
					});
				}
			}
		}
		if (state.deaths.Count > 2000)
		{
			state.deaths.RemoveRange(0, state.deaths.Count - 2000);
			HashSet<string> retained = new HashSet<string>(state.deaths.Select((StoryEvent d) => d.id));
			state.links.RemoveWhere((string k) => !retained.Contains(k.Split(new char[1] { '/' })[0]) || !retained.Contains(k.Split(new char[1] { '/' })[1]));
		}
		if (state.deathIds.Count > 8192)
		{
			HashSet<string> keep = new HashSet<string>(state.deaths.Select((StoryEvent d) => d.victim.id));
			string[] array3 = state.deathIds.Keys.Where((string k) => !keep.Contains(k)).Take(state.deathIds.Count - 8192).ToArray();
			foreach (string key in array3)
			{
				state.deathIds.Remove(key);
			}
		}
		return true;
	}
}