Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Valheim Chat Plus v0.1.17
BepInEx/plugins/ValheimChatPlus/ValheimChatPlus.dll
Decompiled 3 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using UnityEngine; [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("ValheimChatPlus")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Global chat, whispers, pings and player notifications for Valheim.")] [assembly: AssemblyFileVersion("0.1.16.0")] [assembly: AssemblyInformationalVersion("0.1.16")] [assembly: AssemblyProduct("Valheim Chat Plus")] [assembly: AssemblyTitle("ValheimChatPlus")] [assembly: AssemblyVersion("0.1.16.0")] namespace CrossplayDirectChat; [BepInPlugin("local.crossplaydirectchat", "Valheim Chat Plus", "0.1.16")] public sealed class Plugin : BaseUnityPlugin { private sealed class Link { public object Peer; public object Rpc; public bool Ready; public bool Warned; public bool Local; public string Color; public string Identity; public string PresenceName; public long OwnPingUid; public string OwnPingName; public Rate ControlRate = new Rate(); public PingRate Pings = new PingRate(); public PingRate OutgoingPings = new PingRate(); public float NextHello; public Rate Rate = new Rate(); public Rate EventRate = new Rate(); public bool Sleeping; public float LastDeathAt; } private sealed class PingLabel { internal string Name; } private const string Hello = "CDC0110_Hello"; private const string Submit = "CDC0110_Submit"; private const string Deliver = "CDC0110_Deliver"; private const string Control = "CDC0110_Control"; private const string Notice = "CDC0110_Notice"; private static Plugin self; private Type netType; private Type chatType; private Type rpcType; private Harmony harmony; private FieldInfo hideTimer; private ConfigEntry<bool> defaultGlobal; private bool fallbackNotified; private object currentNet; private float nextScan; private readonly Dictionary<object, Link> links = new Dictionary<object, Link>(); private readonly Dictionary<string, string> names = new Dictionary<string, string>(); private ColorStore colorStore; private bool colorStoreFailed; private readonly HashSet<string> muted = new HashSet<string>(); private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private Link hostLink; private object hostPlayer; private MethodInfo hostUid; private MethodInfo hostPlayerId; private MethodInfo hostPlayerName; private MethodInfo hostIsDedicated; private bool hostWarning; private float nextHostAttempt; private const string PingSubmit = "CDC0110_PingSubmit"; private const string PingDeliver = "CDC0110_PingDeliver"; private Harmony pingHarmony; private Type pingUserType; private Type pingMapType; private FieldInfo pingLocalPlayer; private FieldInfo pingUserName; private FieldInfo pingMapMode; private FieldInfo pingNoMapDistance; private MethodInfo pingInIntro; private MethodInfo pingAddWorldText; private object pingType; private object pingNoMap; private bool pingsReady; private float nextPingError; private readonly ConditionalWeakTable<object, PingLabel> pingNames = new ConditionalWeakTable<object, PingLabel>(); private const string WhisperSubmit = "CDC0110_WhisperSubmit"; private const string WhisperDeliver = "CDC0110_WhisperDeliver"; private const string WhisperRoster = "CDC0110_WhisperRoster"; private const string WhisperNotice = "CDC0110_WhisperNotice"; private WhisperPlayer[] roster = Array.Empty<WhisperPlayer>(); private readonly WhisperCompletion completion = new WhisperCompletion(); private readonly ReplyState reply = new ReplyState(); private bool rosterDirty; private bool tabReady; private Harmony tabHarmony; private ConfigEntry<bool> whisperTab; private Func<bool> tabPressed; private Func<object, bool> inputFocused; private Func<object, bool> inputComposing; private Func<object, string> inputText; private Func<object, int> inputCaret; private Func<object, int> inputAnchor; private Func<object, int> inputFocus; private Action<object, string> replaceInput; private Action<object, int> replaceCaret; private Type localizationType; private MethodInfo selectedLanguage; private const string Presence = "CDC0110_Presence"; private const string EventSubmit = "CDC0112_EventSubmit"; private const string EventDeliver = "CDC0112_EventDeliver"; private Harmony eventHarmony; private Type playerType; private Type characterType; private Type zdoType; private Type zdoVarsType; private Type znetViewType; private FieldInfo localPlayer; private FieldInfo lastHit; private FieldInfo netView; private FieldInfo zdoUid; private FieldInfo inBedField; private FieldInfo characterName; private MethodInfo localize; private MethodInfo getZdo; private int inBedHash; private bool sleepPatchReady; private bool sleepIntentPatchReady; private bool sleepStatePatchReady; private bool deathPatchReady; private bool localInBedState; private ConfigEntry<bool> messageSoundEnabled; private ConfigEntry<float> messageSoundVolume; private readonly MessageSoundGate soundGate = new MessageSoundGate(); private bool soundAttempted; private Object messageClip; private Object messageSource; private Action<float> playMessageClip; private Func<bool> messageClipPlaying; private Func<float> gameSfxVolume; private bool Server { get { if (currentNet != null) { return (bool)Call(currentNet, "IsServer"); } return false; } } private static object Get(object o, string n) { return AccessTools.Field(o.GetType(), n).GetValue(o); } private static object Instance(Type t) { object obj = AccessTools.Property(t, "instance")?.GetValue(null); if (obj == null) { FieldInfo fieldInfo = AccessTools.Field(t, "m_instance"); if ((object)fieldInfo == null) { return null; } obj = fieldInfo.GetValue(null); } return obj; } private static object Call(object o, string n, params object[] args) { MethodInfo methodInfo = AccessTools.Method(o.GetType(), n, args.Select((object x) => x.GetType()).ToArray(), (Type[])null); if (methodInfo == null) { throw new MissingMethodException(o.GetType().FullName, n); } return methodInfo.Invoke(o, args); } private bool Active(Link l) { if (links.TryGetValue(l.Rpc, out var value) && value == l) { if (!l.Local) { return (bool)Call(l.Peer, "IsReady"); } if (l == hostLink) { return Server; } return false; } return false; } private bool ServerLink(Link l) { if (!l.Local) { if (currentNet != null) { return Call(currentNet, "GetServerPeer") == l.Peer; } return false; } if (l == hostLink) { return Server; } return false; } private void Awake() { //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Expected O, but got Unknown try { netType = AccessTools.TypeByName("ZNet"); chatType = AccessTools.TypeByName("Chat"); rpcType = AccessTools.TypeByName("ZRpc"); if (netType == null || chatType == null || rpcType == null) { throw new Exception("Valheim types missing"); } hideTimer = AccessTools.Field(chatType, "m_hideTimer"); if (hideTimer == null || hideTimer.FieldType != typeof(float)) { throw new MissingFieldException("Chat.m_hideTimer"); } defaultGlobal = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "DefaultGlobal", true, "Prefer Valheim Chat Plus global chat for ordinary text. Uses vanilla without a compatible server. Toggle with /chatmode; saved across restarts."); self = this; harmony = new Harmony("local.crossplaydirectchat"); harmony.Patch((MethodBase)AccessTools.Method(chatType, "InputText", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "Input", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); SetupLocalization(); SetupWhispers(); ConfigureMessageSound(); SetupPings(); SetupPlayerEvents(); SetupHosting(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"VALHEIM_CHAT_PLUS_READY 0.1.16 — /chathelp; host or dedicated server + clients required"); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)ex); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } ((Behaviour)this).enabled = false; } } private void Register(Link l, string name, Action<string> action) { MethodInfo methodInfo = rpcType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Register" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1).MakeGenericMethod(typeof(string)); Type parameterType = methodInfo.GetParameters()[1].ParameterType; ParameterExpression[] array = (from p in parameterType.GetMethod("Invoke").GetParameters() select Expression.Parameter(p.ParameterType)).ToArray(); Action<string> value = delegate(string s) { try { if (Active(l)) { action(s); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_HANDLER: " + ex.GetType().Name)); } }; Delegate obj = Expression.Lambda(parameterType, Expression.Invoke(Expression.Constant(value), array[1]), array).Compile(); methodInfo.Invoke(l.Rpc, new object[2] { name, obj }); } private void Send(Link l, string name, string value) { if (l.Local) { DispatchHost(l, name, value); return; } Call(l.Rpc, "Invoke", name, new object[1] { value }); } private void Update() { if (Time.realtimeSinceStartup < nextScan) { return; } nextScan = Time.realtimeSinceStartup + 0.5f; try { object obj = Instance(netType); if (obj != currentNet) { links.Clear(); ResetHost(); muted.Clear(); names.Clear(); ResetWhispers(); colorStore = null; colorStoreFailed = false; localInBedState = false; fallbackNotified = false; currentNet = obj; } if (obj == null) { return; } EnsureHost(); object[] peers = ((IEnumerable)Call(obj, "GetPeers")).Cast<object>().ToArray(); object[] array = (from p in links where !p.Value.Local && !peers.Any((object x) => x == p.Value.Peer) select p.Key).ToArray(); foreach (object key in array) { Link l = links[key]; links.Remove(key); RosterChanged(); AnnounceLeave(l); } array = peers; foreach (object obj2 in array) { if (!(bool)Call(obj2, "IsReady")) { continue; } object obj3 = Get(obj2, "m_rpc"); if (!links.TryGetValue(obj3, out var l2)) { l2 = new Link { Peer = obj2, Rpc = obj3 }; links.Add(obj3, l2); Register(l2, "CDC0110_Hello", delegate(string s) { OnHello(l2, s); }); Register(l2, "CDC0110_Submit", delegate(string s) { OnSubmit(l2, s); }); Register(l2, "CDC0110_Deliver", delegate(string s) { OnDeliver(l2, s); }); Register(l2, "CDC0110_Control", delegate(string s) { OnControl(l2, s); }); Register(l2, "CDC0110_Notice", delegate(string s) { OnNotice(l2, s); }); Register(l2, "CDC0110_PingSubmit", delegate(string s) { OnPingSubmit(l2, s); }); Register(l2, "CDC0110_PingDeliver", delegate(string s) { OnPingDeliver(l2, s); }); Register(l2, "CDC0110_WhisperSubmit", delegate(string s) { OnWhisperSubmit(l2, s); }); Register(l2, "CDC0110_WhisperDeliver", delegate(string s) { OnWhisperDeliver(l2, s); }); Register(l2, "CDC0110_WhisperRoster", delegate(string s) { OnWhisperRoster(l2, s); }); Register(l2, "CDC0110_WhisperNotice", delegate(string s) { OnWhisperNotice(l2, s); }); Register(l2, "CDC0110_Presence", delegate(string s) { OnPresence(l2, s); }); Register(l2, "CDC0112_EventSubmit", delegate(string s) { OnEventSubmit(l2, s); }); Register(l2, "CDC0112_EventDeliver", delegate(string s) { OnEventDeliver(l2, s); }); } if (!Server && ServerLink(l2) && !l2.Ready && Time.realtimeSinceStartup >= l2.NextHello) { l2.NextHello = Time.realtimeSinceStartup + 5f; Send(l2, "CDC0110_Hello", "0114"); } } FlushRoster(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_UPDATE: " + ex.GetType().Name + " " + ex.Message)); nextScan += 5f; } } private void OnHello(Link l, string s) { if (Server) { if (s != "0114" || !l.Rate.Allow(Time.realtimeSinceStartup)) { return; } if (!l.Ready) { try { EnsureColors(); object obj = Call(l.Rpc, "GetSocket"); string text = ColorRules.TransportIdentity(obj.GetType().Name, (string)Call(obj, "GetHostName")); l.Color = colorStore.GetOrAssign(text); l.Identity = text; l.Ready = true; rosterDirty = true; } catch (Exception ex) { if (!l.Warned) { l.Warned = true; ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_COLOR: " + (ex.InnerException ?? ex).Message)); } Send(l, "CDC0110_Hello", "color-error"); return; } } Send(l, "CDC0110_Hello", PingRules.Welcome(PeerUid(l), PeerName(l))); AnnounceJoin(l); } else if (ServerLink(l)) { long uid; string name; if (s == "color-error" && !l.Warned) { l.Warned = true; ShowText("color.error"); } else if (!l.Ready && PingRules.ReadWelcome(s, out uid, out name)) { l.OwnPingUid = uid; l.OwnPingName = name; l.Ready = true; fallbackNotified = false; PrepareMessageSound(); ShowText("welcome", ActiveMode()); } } } private void OnSubmit(Link l, string s) { if (!Server || !l.Ready || !l.Rate.Allow(Time.realtimeSinceStartup) || s == null || s.Length > 400) { return; } string text = Rules.Clean(s, 400); if (text.Length == 0) { return; } string id = PeerUid(l).ToString(CultureInfo.InvariantCulture); string name = PeerName(l); string value = ColorPacket.Pack(id, name, l.Color, text); Link[] array = links.Values.ToArray(); foreach (Link link in array) { if (link.Ready && Active(link)) { try { Send(link, "CDC0110_Deliver", value); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_SEND: " + ex.GetType().Name)); } } } } private void OnDeliver(Link l, string s) { if (!CanReceive(l) || !ColorPacket.TryRead(s, out var id, out var name, out var color, out var text)) { return; } names[id] = name; if (!muted.Contains(id)) { AddLine("<color=" + color + "><b>" + name + "</b></color>: " + text); if (id != l.OwnPingUid.ToString(CultureInfo.InvariantCulture)) { PlayMessageSound(); } } } private void OnNotice(Link l, string s) { if (CanReceive(l) && NoticePacket.TryFormat(s, Spanish(), out var text)) { Show(text); } } private void OnControl(Link l, string s) { if (!Server || !l.Ready || s == null || s.Length > 240 || !l.ControlRate.Allow(Time.realtimeSinceStartup)) { return; } try { if (s.Equals("/cplayers", StringComparison.OrdinalIgnoreCase)) { foreach (Link value in links.Values) { if (value.Ready && Active(value)) { SendNotice(l, "players.row", PeerName(value), value.Identity, value.Color, PeerUid(value).ToString(CultureInfo.InvariantCulture)); } } return; } bool flag = l.Local && l == hostLink; if (!flag) { string text = (string)Call(Call(l.Rpc, "GetSocket"), "GetHostName"); flag = (bool)Call(currentNet, "IsAdmin", text); } if (!flag) { SendNotice(l, "color.admin"); return; } string target; string text2 = colorStore.AdminCommand(flag, s, out target, (string name) => NameTargets.Resolve(name, from p in links.Values where p.Ready && Active(p) select new KeyValuePair<string, string>(p.Identity, PeerName(p)))); foreach (Link value2 in links.Values) { if (value2.Identity == target) { value2.Color = text2; } } SendNotice(l, "color.saved", target, text2); } catch (FormatException ex) { SendNotice(l, Texts.Has(ex.Message) ? ex.Message : "color.failed"); } catch (InvalidOperationException ex2) { SendNotice(l, Texts.Has(ex2.Message) ? ex2.Message : "color.failed"); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_ADMIN: " + (ex3.InnerException ?? ex3).Message)); SendNotice(l, "color.failed"); } } private void AddLine(string s) { object obj = Instance(chatType); if (obj != null) { hideTimer.SetValue(obj, 0f); Call(obj, "AddString", s); } } private void Show(string s) { AddLine("[Valheim Chat Plus] " + s); } private static bool Input(object __instance) { if ((Object)(object)self == (Object)null) { return true; } object obj = Get(__instance, "m_input"); string text = (string)AccessTools.Property(obj.GetType(), "text").GetValue(obj); InputRoute inputRoute = Rules.Route(text, self.defaultGlobal.Value); if (inputRoute == InputRoute.Vanilla) { return true; } try { if (inputRoute == InputRoute.Global) { if (Rules.ResolveAvailability(inputRoute, self.GlobalAvailable()) == InputRoute.Vanilla) { if (!self.fallbackNotified) { self.fallbackNotified = true; self.ShowText("fallback"); } return true; } self.fallbackNotified = false; self.SendGlobal(text); } else { self.Command(text.Trim()); } } catch (Exception ex) { self.ShowText("action.error"); ((BaseUnityPlugin)self).Logger.LogWarning((object)ex); } return false; } private void Command(string text) { completion.Reset(); if (text.Equals("/chathelp", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/chathelp ", StringComparison.OrdinalIgnoreCase)) { ShowHelp(); } else if (WhisperRules.IsCommand(text)) { SendWhisper(text); } else if (ReplyState.IsCommand(text)) { SendReply(text); } else if (text.Equals("/chatmode", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/chatmode ", StringComparison.OrdinalIgnoreCase)) { string text2 = text.Substring(9).Trim().ToLowerInvariant(); if (text2 == "status") { ShowText("mode.status", PreferredMode(), ActiveMode()); return; } if (text2 != "" && text2 != "global" && text2 != "vanilla") { ShowText("mode.usage"); return; } bool value = defaultGlobal.Value; try { defaultGlobal.Value = ((text2 == "") ? (!value) : (text2 == "global")); ((BaseUnityPlugin)this).Config.Save(); } catch { try { defaultGlobal.Value = value; } catch { } ShowText("mode.save_error"); return; } ShowText("mode.saved", PreferredMode(), ActiveMode()); } else if (text.Equals("/cplayers", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/cplayers ", StringComparison.OrdinalIgnoreCase) || text.Equals("/chatcolor", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/chatcolor ", StringComparison.OrdinalIgnoreCase)) { Link link = MessagingServer(); if (link == null) { ShowText("offline"); } else { Send(link, "CDC0110_Control", text); } } else if (text.StartsWith("/cmute", StringComparison.OrdinalIgnoreCase)) { string text3 = text.Substring(6).Trim(); if (!long.TryParse(text3, out var _)) { ShowText("mute.usage"); } else if (muted.Remove(text3)) { ShowText("mute.off", text3); } else { muted.Add(text3); ShowText("mute.on", text3); } } else { SendGlobal((text.Length > 2) ? text.Substring(2) : ""); } } private void SendGlobal(string text) { string text2 = Rules.Clean(text, 400); if (text2.Length == 0) { ShowText("global.usage"); return; } Link link = MessagingServer(); if (link == null) { ShowText("offline"); } else { Send(link, "CDC0110_Submit", text2); } } private void OnDestroy() { ReleaseMessageSound(); Harmony obj = eventHarmony; if (obj != null) { obj.UnpatchSelf(); } Harmony obj2 = tabHarmony; if (obj2 != null) { obj2.UnpatchSelf(); } Harmony obj3 = pingHarmony; if (obj3 != null) { obj3.UnpatchSelf(); } Harmony obj4 = harmony; if (obj4 != null) { obj4.UnpatchSelf(); } if ((Object)(object)self == (Object)(object)this) { self = null; } } private void SetupHosting() { hostUid = AccessTools.Method(netType, "GetUID", Type.EmptyTypes, (Type[])null); hostIsDedicated = AccessTools.Method(netType, "IsDedicated", Type.EmptyTypes, (Type[])null); if (playerType != null) { hostPlayerId = AccessTools.Method(playerType, "GetPlayerID", Type.EmptyTypes, (Type[])null); hostPlayerName = AccessTools.Method(playerType, "GetPlayerName", Type.EmptyTypes, (Type[])null); } } private void ResetHost() { hostLink = null; hostPlayer = null; hostWarning = false; nextHostAttempt = 0f; } private void EnsureColors() { if (colorStoreFailed) { throw new InvalidOperationException("Color file unavailable; repair it and restart the server."); } if (colorStore != null) { return; } try { colorStore = ColorStore.Load(Path.Combine(Paths.ConfigPath, "CrossplayDirectChat", "colors-v1.tsv")); } catch { colorStoreFailed = true; throw; } } private void EnsureHost() { if (!Server || Time.realtimeSinceStartup < nextHostAttempt) { return; } object obj = localPlayer?.GetValue(null); if (obj == null || (hostLink != null && obj == hostPlayer)) { return; } try { if (hostUid == null || hostPlayerId == null || hostPlayerName == null || hostIsDedicated == null) { throw new MissingMemberException("Local host identity methods unavailable."); } if ((bool)hostIsDedicated.Invoke(currentNet, null)) { return; } long num = Convert.ToInt64(hostUid.Invoke(currentNet, null), CultureInfo.InvariantCulture); long num2 = Convert.ToInt64(hostPlayerId.Invoke(obj, null), CultureInfo.InvariantCulture); string text = Rules.Clean(hostPlayerName.Invoke(obj, null) as string, 64); if (num == 0L || num2 == 0L || text.Length == 0) { return; } string text2 = "Host_" + num2.ToString(CultureInfo.InvariantCulture); if (hostLink != null && hostLink.Identity == text2 && hostLink.OwnPingUid == num) { hostPlayer = obj; hostLink.OwnPingName = text; hostLink.PresenceName = text; hostLink.Sleeping = false; localInBedState = false; rosterDirty = true; return; } EnsureColors(); string orAssign = colorStore.GetOrAssign(text2); if (hostLink != null) { links.Remove(hostLink.Rpc); AnnounceLeave(hostLink); } hostLink = new Link { Local = true, Ready = true, Rpc = new object(), Identity = text2, Color = orAssign, OwnPingUid = num, OwnPingName = text }; hostPlayer = obj; links.Add(hostLink.Rpc, hostLink); localInBedState = false; fallbackNotified = false; rosterDirty = true; PrepareMessageSound(); ShowText("welcome", ActiveMode()); AnnounceJoin(hostLink); ((BaseUnityPlugin)this).Logger.LogInfo((object)"VALHEIM_CHAT_PLUS_HOST_READY 0.1.16"); } catch (Exception ex) { nextHostAttempt = Time.realtimeSinceStartup + 5f; if (!hostWarning) { hostWarning = true; ((BaseUnityPlugin)this).Logger.LogWarning((object)("VALHEIM_CHAT_PLUS_HOST_UNAVAILABLE: " + (ex.InnerException ?? ex).Message)); ShowText("host.error"); } } } private long PeerUid(Link l) { if (!l.Local) { return Convert.ToInt64(Get(l.Peer, "m_uid"), CultureInfo.InvariantCulture); } return l.OwnPingUid; } private Link MessagingServer() { if (currentNet == null) { return null; } if (Server) { EnsureHost(); if (hostLink == null || !hostLink.Ready || !Active(hostLink)) { return null; } return hostLink; } foreach (Link value in links.Values) { if (value.Ready && Active(value) && ServerLink(value)) { return value; } } return null; } private bool CanReceive(Link l) { if (l != null && l.Ready && Active(l)) { if (!l.Local) { if (!Server) { return ServerLink(l); } return false; } if (Server) { return l == hostLink; } return false; } return false; } private void DispatchHost(Link l, string channel, string packet) { if (l != hostLink || !l.Ready || !Active(l)) { throw new InvalidOperationException("Inactive local host endpoint."); } switch (channel) { case "CDC0110_Submit": OnSubmit(l, packet); break; case "CDC0110_Control": OnControl(l, packet); break; case "CDC0110_WhisperSubmit": OnWhisperSubmit(l, packet); break; case "CDC0110_PingSubmit": OnPingSubmit(l, packet); break; case "CDC0112_EventSubmit": OnEventSubmit(l, packet); break; case "CDC0110_Deliver": OnDeliver(l, packet); break; case "CDC0110_Notice": OnNotice(l, packet); break; case "CDC0110_WhisperDeliver": OnWhisperDeliver(l, packet); break; case "CDC0110_WhisperNotice": OnWhisperNotice(l, packet); break; case "CDC0110_WhisperRoster": OnWhisperRoster(l, packet); break; case "CDC0110_Presence": OnPresence(l, packet); break; case "CDC0110_PingDeliver": OnPingDeliver(l, packet); break; case "CDC0112_EventDeliver": OnEventDeliver(l, packet); break; default: throw new InvalidOperationException("Unsupported local host channel."); } } private void SetupLocalization() { localizationType = AccessTools.TypeByName("Localization"); if (localizationType != null) { selectedLanguage = AccessTools.Method(localizationType, "GetSelectedLanguage", Type.EmptyTypes, (Type[])null); } } private bool Spanish() { try { if (selectedLanguage == null) { return false; } object obj = Instance(localizationType); return obj != null && Texts.IsSpanish(selectedLanguage.Invoke(obj, null) as string); } catch { return false; } } private string T(string key, params object[] args) { return Texts.Get(key, Spanish(), args); } private void ShowText(string key, params object[] args) { Show(T(key, args)); } private bool GlobalAvailable() { return MessagingServer() != null; } private string PreferredMode() { return T(defaultGlobal.Value ? "mode.global" : "mode.vanilla"); } private string ActiveMode() { return T((defaultGlobal.Value && GlobalAvailable()) ? "mode.global" : "mode.vanilla"); } private void ShowHelp() { for (int i = 1; i <= 7; i++) { ShowText("help." + i); } } private void SendNotice(Link l, string key, params string[] args) { Send(l, "CDC0110_Notice", NoticePacket.Pack(key, args)); } private void SetupPings() { //IL_0229: Unknown result type (might be due to invalid IL or missing references) //IL_0233: Expected O, but got Unknown //IL_024a: Unknown result type (might be due to invalid IL or missing references) //IL_0258: Expected O, but got Unknown //IL_0270: Unknown result type (might be due to invalid IL or missing references) //IL_027e: Expected O, but got Unknown try { Type type = AccessTools.TypeByName("Player"); pingUserType = AccessTools.TypeByName("UserInfo"); pingMapType = AccessTools.TypeByName("Minimap"); pingLocalPlayer = AccessTools.Field(type, "m_localPlayer"); pingUserName = AccessTools.Field(pingUserType, "Name"); pingMapMode = AccessTools.Field(pingMapType, "m_mode"); pingNoMapDistance = AccessTools.Field(pingMapType, "m_nomapPingDistance"); pingInIntro = AccessTools.Method(type, "InIntro", Type.EmptyTypes, (Type[])null); pingAddWorldText = AccessTools.Method(chatType, "AddInworldText", (Type[])null, (Type[])null); MethodInfo methodInfo = AccessTools.Method(chatType, "SendPing", new Type[1] { typeof(Vector3) }, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(pingUserType, "GetDisplayName", Type.EmptyTypes, (Type[])null); if (pingLocalPlayer == null || pingUserName == null || pingMapMode == null || pingNoMapDistance == null || pingInIntro == null || pingAddWorldText == null || methodInfo == null || methodInfo2 == null) { throw new MissingMemberException("Required native ping members unavailable."); } ParameterInfo[] parameters = pingAddWorldText.GetParameters(); if (parameters.Length != 6 || parameters[0].ParameterType != typeof(GameObject) || parameters[1].ParameterType != typeof(long) || parameters[2].ParameterType != typeof(Vector3) || parameters[4].ParameterType != pingUserType || parameters[5].ParameterType != typeof(string)) { throw new MissingMethodException("Unsupported AddInworldText signature."); } pingType = Enum.Parse(parameters[3].ParameterType, "Ping"); pingNoMap = Enum.Parse(pingMapMode.FieldType, "None"); pingHarmony = new Harmony("local.crossplaydirectchat.pings"); pingHarmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "SendDirectPing", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); pingHarmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(Plugin), "DirectPingName", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); pingsReady = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"DIRECT_PINGS_READY 0.1.16"); } catch (Exception ex) { pingsReady = false; Harmony obj = pingHarmony; if (obj != null) { obj.UnpatchSelf(); } ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PINGS_DISABLED: " + ex.Message + "; text chat remains available.")); } } private static bool DirectPingName(object __instance, ref string __result) { if ((Object)(object)self != (Object)null && self.pingNames.TryGetValue(__instance, out var value)) { __result = value.Name; return false; } return true; } private static bool SendDirectPing(Vector3 __0) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)self == (Object)null || !self.pingsReady || self.currentNet == null) { return true; } Link link = self.MessagingServer(); if (link == null) { return true; } try { object? value = self.pingLocalPlayer.GetValue(null); Component val = (Component)((value is Component) ? value : null); if ((Object)(object)val == (Object)null) { return false; } __0.y = val.transform.position.y; string text = PingRules.Pack(__0.x, __0.y, __0.z); if (!PingRules.Position(text, out var _, out var _, out var _) || !link.OutgoingPings.Allow(Time.realtimeSinceStartup)) { return false; } self.RenderDirectPing(link.OwnPingUid, link.OwnPingName, __0.x, __0.y, __0.z); self.Send(link, "CDC0110_PingSubmit", text); } catch (Exception e) { self.PingError(e); } return false; } private void OnPingSubmit(Link l, string s) { if (!Server || !l.Ready || !l.Pings.Allow(Time.realtimeSinceStartup) || !PingRules.Position(s, out var x, out var y, out var z)) { return; } long uid = PeerUid(l); string name = PeerName(l); string value = PingRules.Delivery(uid, name, x, y, z); foreach (Link value2 in links.Values) { if (value2 != l && value2.Ready && Active(value2)) { try { Send(value2, "CDC0110_PingDeliver", value); } catch (Exception e) { PingError(e); } } } } private void OnPingDeliver(Link l, string s) { if (CanReceive(l) && pingsReady && PingRules.Read(s, out var uid, out var name, out var x, out var y, out var z)) { RenderDirectPing(uid, name, x, y, z); } } private void RenderDirectPing(long uid, string name, float x, float y, float z) { //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) try { object? value = pingLocalPlayer.GetValue(null); Component val = (Component)((value is Component) ? value : null); if ((Object)(object)val == (Object)null || (bool)pingInIntro.Invoke(val, null)) { return; } Vector3 val2 = default(Vector3); ((Vector3)(ref val2))..ctor(x, y, z); object obj = Instance(pingMapType); if (obj == null || !pingNoMap.Equals(pingMapMode.GetValue(obj)) || !(Vector3.Distance(val.transform.position, val2) > (float)pingNoMapDistance.GetValue(obj))) { object obj2 = Instance(chatType); if (obj2 != null) { object obj3 = Activator.CreateInstance(pingUserType); pingUserName.SetValue(obj3, name); pingNames.Add(obj3, new PingLabel { Name = name }); pingAddWorldText.Invoke(obj2, new object[6] { null, uid, val2, pingType, obj3, "" }); } } } catch (Exception e) { PingError(e); } } private void PingError(Exception e) { if (!(Time.realtimeSinceStartup < nextPingError)) { nextPingError = Time.realtimeSinceStartup + 5f; ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PING: " + (e.InnerException ?? e).Message)); } } private void SetupWhispers() { //IL_0268: Unknown result type (might be due to invalid IL or missing references) //IL_0272: Expected O, but got Unknown //IL_0289: Unknown result type (might be due to invalid IL or missing references) //IL_0297: Expected O, but got Unknown whisperTab = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "WhisperTabCompletion", true, "Complete /w player names only when Tab is pressed. Disable to compare performance; whispers remain available."); try { Type type = AccessTools.TypeByName("Terminal"); MethodInfo methodInfo = AccessTools.Method(type, "UpdateInput", Type.EmptyTypes, (Type[])null); FieldInfo fieldInfo = AccessTools.Field(type, "m_input"); Type type2 = AccessTools.TypeByName("ZInput"); Type type3 = AccessTools.TypeByName("UnityEngine.KeyCode"); MethodInfo methodInfo2 = AccessTools.Method(type2, "GetKeyDown", new Type[2] { type3, typeof(bool) }, (Type[])null); if (methodInfo == null || fieldInfo == null || methodInfo2 == null) { throw new MissingMemberException("Native input members unavailable."); } ParameterExpression parameterExpression = Expression.Parameter(typeof(object), "terminal"); MemberExpression memberExpression = Expression.Field(Expression.Convert(parameterExpression, type), fieldInfo); inputFocused = InputGetter<bool>(parameterExpression, memberExpression, "isFocused"); inputText = InputGetter<string>(parameterExpression, memberExpression, "text"); inputCaret = InputGetter<int>(parameterExpression, memberExpression, "caretPosition"); inputAnchor = InputGetter<int>(parameterExpression, memberExpression, "selectionAnchorPosition"); inputFocus = InputGetter<int>(parameterExpression, memberExpression, "selectionFocusPosition"); MethodInfo method = AccessTools.Method(fieldInfo.FieldType, "IsCompositionActive", Type.EmptyTypes, (Type[])null); inputComposing = Expression.Lambda<Func<object, bool>>(Expression.Call(memberExpression, method), new ParameterExpression[1] { parameterExpression }).Compile(); ParameterExpression parameterExpression2 = Expression.Parameter(typeof(string), "value"); replaceInput = Expression.Lambda<Action<object, string>>(Expression.Call(memberExpression, AccessTools.Property(fieldInfo.FieldType, "text").SetMethod, parameterExpression2), new ParameterExpression[2] { parameterExpression, parameterExpression2 }).Compile(); ParameterExpression parameterExpression3 = Expression.Parameter(typeof(int), "caret"); replaceCaret = Expression.Lambda<Action<object, int>>(Expression.Call(memberExpression, AccessTools.Property(fieldInfo.FieldType, "caretPosition").SetMethod, parameterExpression3), new ParameterExpression[2] { parameterExpression, parameterExpression3 }).Compile(); tabPressed = Expression.Lambda<Func<bool>>(Expression.Call(methodInfo2, Expression.Constant(Enum.Parse(type3, "Tab"), type3), Expression.Constant(true)), Array.Empty<ParameterExpression>()).Compile(); tabHarmony = new Harmony("local.crossplaydirectchat.whisper-tab"); tabHarmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "CompleteWhisperTab", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); tabReady = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"DIRECT_WHISPER_TAB_READY 0.1.16"); } catch (Exception ex) { tabReady = false; Harmony obj = tabHarmony; if (obj != null) { obj.UnpatchSelf(); } ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_WHISPER_TAB_DISABLED: " + ex.GetType().Name + "; /w remains available.")); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"DIRECT_WHISPERS_READY 0.1.16"); } private static Func<object, T> InputGetter<T>(ParameterExpression obj, Expression field, string name) { return Expression.Lambda<Func<object, T>>(Expression.Property(field, AccessTools.Property(field.Type, name)), new ParameterExpression[1] { obj }).Compile(); } private static bool CompleteWhisperTab(object __instance) { Plugin plugin = self; if ((Object)(object)plugin == (Object)null || !plugin.tabReady || !plugin.whisperTab.Value || !plugin.chatType.IsInstanceOfType(__instance)) { return true; } bool flag = false; try { if (!plugin.tabPressed() || !plugin.inputFocused(__instance)) { return true; } string text = plugin.inputText(__instance); if (!WhisperRules.IsCommand(text)) { plugin.completion.Reset(); return true; } flag = true; if (plugin.inputComposing(__instance) || plugin.inputAnchor(__instance) != plugin.inputFocus(__instance)) { return false; } if (plugin.completion.Complete(text, plugin.inputCaret(__instance), plugin.roster, out var result, out var position)) { plugin.replaceInput(__instance, result); plugin.replaceCaret(__instance, position); } return false; } catch (Exception ex) { plugin.tabReady = false; plugin.completion.Reset(); ((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_WHISPER_TAB_DISABLED: " + ex.GetType().Name)); return !flag; } } private void ResetWhispers() { roster = Array.Empty<WhisperPlayer>(); rosterDirty = false; completion.Reset(); reply.Reset(); soundGate.Reset(); } private void RosterChanged() { if (Server) { rosterDirty = true; return; } roster = Array.Empty<WhisperPlayer>(); completion.Reset(); reply.Reset(); soundGate.Reset(); } private WhisperPlayer PlayerFor(Link l) { return new WhisperPlayer(l.Identity, PeerUid(l), PeerName(l)); } private void FlushRoster() { if (!rosterDirty) { return; } rosterDirty = false; if (!Server) { return; } List<WhisperPlayer> list = new List<WhisperPlayer>(); foreach (Link value2 in links.Values) { if (value2.Ready && Active(value2)) { list.Add(PlayerFor(value2)); } } string value = ""; try { string text = WhisperRules.Roster(list); if (WhisperRules.ReadRoster(text, out var _)) { value = text; } } catch (InvalidOperationException) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"DIRECT_WHISPER_ROSTER_UNAVAILABLE"); } foreach (Link value3 in links.Values) { if (value3.Ready && Active(value3)) { try { Send(value3, "CDC0110_WhisperRoster", value); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_WHISPER_ROSTER: " + ex2.GetType().Name)); } } } } private void OnWhisperRoster(Link l, string packet) { if (CanReceive(l)) { completion.Reset(); roster = (WhisperRules.ReadRoster(packet, out var players) ? players : Array.Empty<WhisperPlayer>()); } } private Link WhisperServer() { return MessagingServer(); } private void SendWhisper(string text) { if (!WhisperRules.Parse(text, out var target, out var message)) { WhisperError("usage"); return; } Link link = WhisperServer(); if (link == null) { WhisperError("offline"); return; } WhisperPlayer whisperPlayer = WhisperRules.Resolve(target, roster); if (whisperPlayer == null) { WhisperError("target"); } else { SendPrivate(link, whisperPlayer, message); } } private void SendReply(string text) { if (!ReplyState.Parse(text, out var message)) { WhisperError("replyusage"); return; } Link link = WhisperServer(); if (link == null) { WhisperError("offline"); return; } if (!reply.HasSender) { WhisperError("noreply"); return; } WhisperPlayer whisperPlayer = reply.Resolve(roster); if (whisperPlayer == null) { WhisperError("target"); } else { SendPrivate(link, whisperPlayer, message); } } private void SendPrivate(Link server, WhisperPlayer player, string message) { if (muted.Contains(player.Uid.ToString(CultureInfo.InvariantCulture))) { WhisperError("muted"); } else { Send(server, "CDC0110_WhisperSubmit", WhisperRules.Submit(player.Id, message)); } } private void OnWhisperSubmit(Link l, string packet) { if (!Server || !l.Ready || !l.Rate.Allow(Time.realtimeSinceStartup)) { return; } if (!WhisperRules.ReadSubmit(packet, out var id, out var message)) { Send(l, "CDC0110_WhisperNotice", "usage"); return; } Link link = null; foreach (Link value in links.Values) { if (value.Ready && value.Identity == id && Active(value)) { if (link != null) { Send(l, "CDC0110_WhisperNotice", "target"); return; } link = value; } } if (link == null) { Send(l, "CDC0110_WhisperNotice", "target"); return; } string packet2 = WhisperRules.Delivery(PlayerFor(l), PlayerFor(link), message); try { WhisperRules.Deliver(l, link, packet2, delegate(Link peer, string value) { Send(peer, "CDC0110_WhisperDeliver", value); }); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_WHISPER_SEND: " + ex.GetType().Name)); Send(l, "CDC0110_WhisperNotice", "send"); } } private void OnWhisperDeliver(Link l, string packet) { if (CanReceive(l) && WhisperRules.ReadDelivery(packet, l.OwnPingUid, out var from, out var fromName, out var _, out var toName, out var message, out var senderId) && (from == l.OwnPingUid || !muted.Contains(from.ToString(CultureInfo.InvariantCulture)))) { AddLine(ChatPresentation.Whisper(from == l.OwnPingUid, fromName, toName, message, Spanish())); if (from != l.OwnPingUid) { reply.Remember(senderId); PlayMessageSound(); } } } private void OnWhisperNotice(Link l, string code) { if (CanReceive(l) && code != null && code.Length <= 16) { WhisperError(code); } } private void WhisperError(string code) { if (Texts.Has("whisper." + code)) { ShowText("whisper." + code); } } private void AnnounceJoin(Link l) { if (Server && l.Ready && l.PresenceName == null) { l.PresenceName = PeerName(l); if (l.PresenceName.Length > 0) { BroadcastPresence(joined: true, l.PresenceName); } } } private void AnnounceLeave(Link l) { if (Server && l.PresenceName != null) { string presenceName = l.PresenceName; l.PresenceName = null; if (presenceName.Length > 0) { BroadcastPresence(joined: false, presenceName); } } } private void BroadcastPresence(bool joined, string name) { string value = ChatPresentation.PresencePacket(joined, name); foreach (Link value2 in links.Values) { try { if (value2.Ready && Active(value2)) { Send(value2, "CDC0110_Presence", value); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PRESENCE_SEND: " + ex.GetType().Name)); } } } private void OnPresence(Link l, string packet) { if (CanReceive(l) && ChatPresentation.ReadPresence(packet, out var joined, out var name)) { AddLine(ChatPresentation.PresenceLine(joined, name, Spanish())); } } private void SetupPlayerEvents() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_033e: Unknown result type (might be due to invalid IL or missing references) //IL_034b: Expected O, but got Unknown //IL_03ed: Unknown result type (might be due to invalid IL or missing references) //IL_03fb: Expected O, but got Unknown //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_02a7: Expected O, but got Unknown eventHarmony = new Harmony("local.crossplaydirectchat.player-events"); playerType = AccessTools.TypeByName("Player"); characterType = AccessTools.TypeByName("Character"); zdoType = AccessTools.TypeByName("ZDO"); zdoVarsType = AccessTools.TypeByName("ZDOVars"); znetViewType = AccessTools.TypeByName("ZNetView"); localPlayer = ((playerType == null) ? null : AccessTools.Field(playerType, "m_localPlayer")); lastHit = ((characterType == null) ? null : AccessTools.Field(characterType, "m_lastHit")); characterName = ((characterType == null) ? null : AccessTools.Field(characterType, "m_name")); netView = ((characterType == null) ? null : AccessTools.Field(characterType, "m_nview")); zdoUid = ((zdoType == null) ? null : AccessTools.Field(zdoType, "m_uid")); getZdo = ((znetViewType == null) ? null : AccessTools.Method(znetViewType, "GetZDO", Type.EmptyTypes, (Type[])null)); inBedField = ((zdoVarsType == null) ? null : AccessTools.Field(zdoVarsType, "s_inBed")); if (localizationType != null) { localize = AccessTools.Method(localizationType, "Localize", new Type[1] { typeof(string) }, (Type[])null); } if (playerType == null || localPlayer == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"DIRECT_PLAYER_EVENTS_DISABLED: Player.m_localPlayer unavailable."); return; } try { if (zdoType == null || inBedField == null || netView == null || getZdo == null) { throw new MissingMemberException("ZDO/ZDOVars.s_inBed/Character.m_nview"); } inBedHash = Convert.ToInt32(inBedField.GetValue(null), CultureInfo.InvariantCulture); MethodInfo methodInfo = AccessTools.Method(zdoType, "Set", new Type[2] { typeof(int), typeof(bool) }, (Type[])null); if (methodInfo == null) { throw new MissingMethodException("ZDO.Set(int,bool)"); } eventHarmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "PlayerInBedFlagChanged", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); sleepIntentPatchReady = true; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_SLEEP_INTENT_DISABLED: " + ex.GetType().Name + " " + ex.Message)); } try { MethodInfo methodInfo2 = playerType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "SetSleeping" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(bool)); if (methodInfo2 == null) { throw new MissingMethodException("Player.SetSleeping(bool)"); } eventHarmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "PlayerSleepingChanged", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); sleepStatePatchReady = true; } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_SLEEP_FALLBACK_DISABLED: " + ex2.GetType().Name + " " + ex2.Message)); } sleepPatchReady = sleepIntentPatchReady || sleepStatePatchReady; try { MethodInfo methodInfo3 = AccessTools.Method(playerType, "OnDeath", Type.EmptyTypes, (Type[])null); if (methodInfo3 == null || lastHit == null) { throw new MissingMemberException("Player.OnDeath/Character.m_lastHit"); } eventHarmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(Plugin), "PlayerDied", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); deathPatchReady = true; } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_DEATH_EVENT_DISABLED: " + ex3.GetType().Name + " " + ex3.Message)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)("DIRECT_PLAYER_EVENTS_READY 0.1.16 sleep=" + sleepPatchReady + " sleepIntent=" + sleepIntentPatchReady + " sleepFallback=" + sleepStatePatchReady + " death=" + deathPatchReady)); } private static void PlayerInBedFlagChanged(object __instance, int __0, bool __1) { Plugin plugin = self; if ((Object)(object)plugin == (Object)null || !plugin.sleepIntentPatchReady || __0 != plugin.inBedHash || !plugin.IsLocalPlayerZdo(__instance)) { return; } try { plugin.ReportSleeping(__1); } catch (Exception ex) { ((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_SLEEP_INTENT: " + ex.GetType().Name)); } } private static void PlayerSleepingChanged(object __instance, bool __0) { Plugin plugin = self; if ((Object)(object)plugin == (Object)null || !plugin.sleepStatePatchReady || !plugin.IsLocalPlayer(__instance)) { return; } try { plugin.ReportSleeping(__0); } catch (Exception ex) { ((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_SLEEP_EVENT: " + ex.GetType().Name)); } } private void ReportSleeping(bool sleeping) { if (localInBedState != sleeping && SendPlayerEvent(PlayerEventRules.SleepSubmit(sleeping))) { localInBedState = sleeping; } } private static void PlayerDied(object __instance) { Plugin plugin = self; if ((Object)(object)plugin == (Object)null || !plugin.deathPatchReady || !plugin.IsLocalPlayer(__instance)) { return; } try { plugin.ReadDeath(__instance, out var hitType, out var attackerName); plugin.SendPlayerEvent(PlayerEventRules.DeathSubmit(hitType, attackerName)); } catch (Exception ex) { ((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_DEATH_EVENT: " + ex.GetType().Name)); } } private bool IsLocalPlayer(object player) { if (player != null) { return localPlayer?.GetValue(null) == player; } return false; } private bool IsLocalPlayerZdo(object zdo) { if (zdo == null) { return false; } try { object obj = localPlayer?.GetValue(null); if (obj == null) { return false; } object obj2 = netView?.GetValue(obj); if (obj2 == null) { return false; } object obj3 = getZdo?.Invoke(obj2, null); if (obj3 == null) { return false; } if (obj3 == zdo) { return true; } if (zdoUid == null) { return false; } object value = zdoUid.GetValue(obj3); object value2 = zdoUid.GetValue(zdo); return value?.Equals(value2) ?? false; } catch { return false; } } private bool SendPlayerEvent(string packet) { Link link = MessagingServer(); if (link == null) { return false; } Send(link, "CDC0112_EventSubmit", packet); return true; } private void ReadDeath(object player, out int hitType, out string attackerName) { hitType = 0; attackerName = ""; object obj = lastHit?.GetValue(player); if (obj == null) { return; } FieldInfo fieldInfo = AccessTools.Field(obj.GetType(), "m_hitType"); if (fieldInfo != null) { try { int num = Convert.ToInt32(fieldInfo.GetValue(obj), CultureInfo.InvariantCulture); if (num >= 0 && num <= 24) { hitType = num; } } catch { } } try { object obj3 = AccessTools.Method(obj.GetType(), "GetAttacker", Type.EmptyTypes, (Type[])null)?.Invoke(obj, null); if (obj3 == null) { return; } string text = ""; MethodInfo methodInfo = AccessTools.Method(obj3.GetType(), "GetPlayerName", Type.EmptyTypes, (Type[])null); if (methodInfo != null) { text = methodInfo.Invoke(obj3, null) as string; } bool token = false; if (string.IsNullOrWhiteSpace(text)) { text = characterName?.GetValue(obj3) as string; if (string.IsNullOrWhiteSpace(text)) { MethodInfo methodInfo2 = AccessTools.Method(obj3.GetType(), "GetHoverName", Type.EmptyTypes, (Type[])null); if (methodInfo2 != null) { text = methodInfo2.Invoke(obj3, null) as string; } } token = text?.StartsWith("$", StringComparison.Ordinal) ?? false; } attackerName = (string.IsNullOrWhiteSpace(text) ? "" : DeathName.Pack(text, token)); } catch { attackerName = ""; } } private string LocalizeName(string value) { string text = Rules.Clean(value, 80); if (text.Length == 0) { return ""; } try { if (localize != null && text.IndexOf('$') >= 0) { string text2 = localize.Invoke(Instance(localizationType), new object[1] { text }) as string; if (!string.IsNullOrWhiteSpace(text2)) { text = Rules.Clean(text2, 80); } } } catch { } return text; } private void OnEventSubmit(Link l, string packet) { if (!Server || !l.Ready || !l.EventRate.Allow(Time.realtimeSinceStartup) || !PlayerEventRules.ReadSubmit(packet, out var kind, out var sleeping, out var hitType, out var attacker)) { return; } switch (kind) { case PlayerEventKind.Sleep: if (l.Sleeping != sleeping) { l.Sleeping = sleeping; if (sleeping) { BroadcastPlayerEvent(PlayerEventRules.SleepDelivery(PeerName(l), SleepingCount(), ActivePlayerCount())); } } break; case PlayerEventKind.Death: { float realtimeSinceStartup = Time.realtimeSinceStartup; if (!(l.LastDeathAt > 0f) || !(realtimeSinceStartup - l.LastDeathAt < 2f)) { l.LastDeathAt = realtimeSinceStartup; l.Sleeping = false; BroadcastPlayerEvent(PlayerEventRules.DeathDelivery(PeerName(l), hitType, attacker)); } break; } } } private void OnEventDeliver(Link l, string packet) { if (CanReceive(l) && PlayerEventRules.ReadDelivery(packet, out var kind, out var name, out var sleeping, out var total, out var hitType, out var attacker)) { switch (kind) { case PlayerEventKind.Sleep: AddLine(PlayerEventRules.SleepLine(name, sleeping, total, Spanish())); break; case PlayerEventKind.Death: AddLine(PlayerEventRules.DeathLine(name, hitType, DeathName.Display(attacker, LocalizeName), Spanish())); break; } } } private void BroadcastPlayerEvent(string packet) { Link[] array = links.Values.ToArray(); foreach (Link link in array) { if (link.Ready && Active(link)) { try { Send(link, "CDC0112_EventDeliver", packet); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PLAYER_EVENT_SEND: " + ex.GetType().Name)); } } } } private string PeerName(Link l) { if (!l.Local) { return Rules.Clean((string)Get(l.Peer, "m_playerName"), 64); } return l.OwnPingName; } private int SleepingCount() { return links.Values.Count((Link x) => x.Ready && x.Sleeping && Active(x)); } private int ActivePlayerCount() { return links.Values.Count((Link x) => x.Ready && Active(x)); } private void ConfigureMessageSound() { messageSoundEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Notifications", "MessageSoundEnabled", true, "Subtle sound for incoming Valheim Chat Plus global messages and whispers. Restart the client after changing this setting."); messageSoundVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Notifications", "MessageSoundVolume", 0.2f, "Notification volume, 0 to 1, additionally controlled by the game's GUI/SFX mixer. Default: 0.20."); } private void PrepareMessageSound() { //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_01fc: Expected O, but got Unknown if (soundAttempted || !messageSoundEnabled.Value) { return; } soundAttempted = true; string text = "types"; try { Type type = AccessTools.TypeByName("UnityEngine.AudioClip"); Type type2 = AccessTools.TypeByName("UnityEngine.AudioSource"); Type type3 = AccessTools.TypeByName("AudioMan"); if (type == null || type2 == null) { throw new MissingMemberException("Unity audio types unavailable."); } text = "game mixer"; object obj = ((type3 == null) ? null : Instance(type3)); object obj2 = ((obj == null) ? null : AccessTools.Field(type3, "m_guiMixer")?.GetValue(obj)); if (obj2 == null && type3 != null) { MethodInfo methodInfo = AccessTools.Method(type3, "GetSFXVolume", Type.EmptyTypes, (Type[])null); if (methodInfo != null && methodInfo.IsStatic && methodInfo.ReturnType == typeof(float)) { gameSfxVolume = Expression.Lambda<Func<float>>(Expression.Call(methodInfo), Array.Empty<ParameterExpression>()).Compile(); } } text = "embedded WAV"; float[] array; int frequency; using (Stream stream = typeof(Plugin).Assembly.GetManifestResourceStream("CrossplayDirectChat.message.wav")) { array = MessageWave.Read(stream, out frequency); } text = "clip"; MethodInfo methodInfo2 = AccessTools.Method(type, "Create", new Type[5] { typeof(string), typeof(int), typeof(int), typeof(int), typeof(bool) }, (Type[])null); if (methodInfo2 == null) { throw new MissingMethodException("AudioClip.Create"); } messageClip = (Object)methodInfo2.Invoke(null, new object[5] { "Valheim Chat Plus message", array.Length, 1, frequency, false }); MethodInfo methodInfo3 = AccessTools.Method(type, "SetData", new Type[2] { typeof(float[]), typeof(int) }, (Type[])null); if (methodInfo3 == null) { throw new MissingMethodException("AudioClip.SetData"); } if (!(bool)methodInfo3.Invoke(messageClip, new object[2] { array, 0 })) { throw new InvalidOperationException("Audio data rejected."); } text = "source"; messageSource = (Object)(object)((Component)this).gameObject.AddComponent(type2); SetAudioProperty(type2, "playOnAwake", false); SetAudioProperty(type2, "loop", false); SetAudioProperty(type2, "spatialBlend", 0f); SetAudioProperty(type2, "volume", 1f); if (obj2 != null) { SetAudioProperty(type2, "outputAudioMixerGroup", obj2); } text = "playback delegates"; ConstantExpression constantExpression = Expression.Constant(messageSource, type2); ParameterExpression parameterExpression = Expression.Parameter(typeof(float), "volume"); MethodInfo methodInfo4 = AccessTools.Method(type2, "PlayOneShot", new Type[2] { type, typeof(float) }, (Type[])null); PropertyInfo propertyInfo = AccessTools.Property(type2, "isPlaying"); if (methodInfo4 == null || propertyInfo == null) { throw new MissingMemberException("AudioSource playback members"); } playMessageClip = Expression.Lambda<Action<float>>(Expression.Call(constantExpression, methodInfo4, Expression.Constant(messageClip, type), parameterExpression), new ParameterExpression[1] { parameterExpression }).Compile(); messageClipPlaying = Expression.Lambda<Func<bool>>(Expression.Property(constantExpression, propertyInfo), Array.Empty<ParameterExpression>()).Compile(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("DIRECT_MESSAGE_SOUND_READY 0.1.16 route=" + ((obj2 == null) ? "default+sfx-volume" : "gui-mixer"))); } catch (Exception ex) { ReleaseMessageSound(); ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_MESSAGE_SOUND_DISABLED stage=" + text + " error=" + ex.GetType().Name + "; chat remains available.")); } } private void SetAudioProperty(Type sourceType, string name, object value) { PropertyInfo propertyInfo = AccessTools.Property(sourceType, name); if (propertyInfo == null || propertyInfo.SetMethod == null) { throw new MissingMemberException("AudioSource." + name); } propertyInfo.SetValue(messageSource, value); } private void PlayMessageSound() { if (playMessageClip == null || !messageSoundEnabled.Value) { return; } float num = MessageSoundGate.Volume(messageSoundVolume.Value); if (num <= 0f) { return; } try { if (!messageClipPlaying() && soundGate.Allow(Time.realtimeSinceStartup)) { if (gameSfxVolume != null) { num = MessageSoundGate.Volume(num * gameSfxVolume()); } if (!(num <= 0f)) { playMessageClip(num); } } } catch (Exception ex) { ReleaseMessageSound(); ((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_MESSAGE_SOUND_DISABLED: " + ex.GetType().Name)); } } private void ReleaseMessageSound() { playMessageClip = null; messageClipPlaying = null; gameSfxVolume = null; if (messageSource != (Object)null) { Object.Destroy(messageSource); } if (messageClip != (Object)null) { Object.Destroy(messageClip); } messageSource = (messageClip = null); } } internal static class Texts { private static readonly Dictionary<string, string[]> Entries = new Dictionary<string, string[]>(StringComparer.Ordinal) { { "host.error", new string[2] { "Local mod chat could not start. Check LogOutput.log; ordinary text uses vanilla.", "No se pudo iniciar el chat local del mod. Revisá LogOutput.log; el texto normal usa vanilla." } }, { "color.error", new string[2] { "The server could not load or save your color. Check the server log.", "El servidor no pudo cargar o guardar tu color. Revisá el log del servidor." } }, { "welcome", new string[2] { "Connected. Chat: {0}. /chathelp for commands.", "Conectado. Chat: {0}. /chathelp para ver los comandos." } }, { "mode.global", new string[2] { "Global", "Global" } }, { "mode.vanilla", new string[2] { "Vanilla", "Vanilla" } }, { "mode.status", new string[2] { "Preferred chat: {0}. Active chat: {1}.", "Chat preferido: {0}. Chat activo: {1}." } }, { "mode.usage", new string[2] { "Usage: /chatmode [global|vanilla|status]. Without an argument, toggles and saves.", "Uso: /chatmode [global|vanilla|status]. Sin argumento alterna y guarda." } }, { "mode.save_error", new string[2] { "Could not save the chat mode. Check BepInEx/config permissions.", "No se pudo guardar el modo de chat. Revisá los permisos de BepInEx/config." } }, { "mode.saved", new string[2] { "Preferred chat saved: {0}. Active chat: {1}. Other mod features remain enabled.", "Chat preferido guardado: {0}. Chat activo: {1}. Las demás funciones del mod siguen activas." } }, { "fallback", new string[2] { "Global chat is unavailable. Using vanilla until a compatible server connects.", "El chat global no está disponible. Se usa vanilla hasta conectar con un servidor compatible." } }, { "action.error", new string[2] { "The action could not be completed. Check LogOutput.log.", "No se completó la acción. Revisá LogOutput.log." } }, { "offline", new string[2] { "Not connected to Valheim Chat Plus. The host/server needs a compatible mod version; try again shortly.", "Sin conexión a Valheim Chat Plus. El anfitrión/servidor necesita una versión compatible del mod; intentá de nuevo en unos segundos." } }, { "mute.usage", new string[2] { "Usage: /cmute ID (see /cplayers). Repeat to unmute.", "Uso: /cmute ID (consultá /cplayers). Repetir para desbloquear." } }, { "mute.off", new string[2] { "Unmuted {0}.", "Desbloqueado {0}." } }, { "mute.on", new string[2] { "Muted {0}.", "Silenciado {0}." } }, { "global.usage", new string[2] { "Usage: /c message (maximum 400 characters; global).", "Uso: /c mensaje (máximo 400 caracteres; global)." } }, { "global.server", new string[2] { "Global chat requires the mod on the host/server and clients.", "El chat global requiere el mod en el anfitrión/servidor y en los clientes." } }, { "color.admin", new string[2] { "Only server administrators can change colors.", "Solo los administradores del servidor pueden cambiar colores." } }, { "color.saved", new string[2] { "Color saved: {0} = {1}. Applies to new messages.", "Color guardado: {0} = {1}. Se aplica a los mensajes nuevos." } }, { "color.failed", new string[2] { "Could not complete the change. Check the server log; no new color was confirmed.", "No se pudo completar el cambio. Revisá el log del servidor; no se confirmó un color nuevo." } }, { "color.usage", new string[2] { "Usage: /chatcolor set NAME_OR_ID #RRGGBB | /chatcolor reset NAME_OR_ID", "Uso: /chatcolor set NOMBRE_O_ID #RRGGBB | /chatcolor reset NOMBRE_O_ID" } }, { "color.unknown", new string[2] { "Unknown ID. Copy the stable ID from /cplayers.", "ID desconocido. Copiá el ID estable de /cplayers." } }, { "color.invalid", new string[2] { "Invalid color. Use #RRGGBB, for example #FF8800.", "Color inválido. Usá #RRGGBB, por ejemplo #FF8800." } }, { "color.used", new string[2] { "That color is already assigned to another player.", "Ese color ya está asignado a otro jugador." } }, { "color.exhausted", new string[2] { "No unused color is available.", "No hay un color libre disponible." } }, { "name.duplicate", new string[2] { "Duplicate name. Use the stable ID from /cplayers.", "Nombre duplicado. Usá el ID estable de /cplayers." } }, { "name.missing", new string[2] { "Player not found. Use their exact connected name or the ID from /cplayers.", "Jugador no encontrado. Usá su nombre exacto conectado o el ID de /cplayers." } }, { "name.quotes", new string[2] { "Invalid quotes. Example: /chatcolor set \"Player name\" #FF69B4", "Comillas inválidas. Ejemplo: /chatcolor set \"Nombre del jugador\" #FF69B4" } }, { "name.quote_full", new string[2] { "Put the full name in quotes.", "Encerrá el nombre completo entre comillas." } }, { "name.empty", new string[2] { "The name cannot be empty.", "El nombre no puede estar vacío." } }, { "players.row", new string[2] { "{0} — ID: {1} — {2} — /cmute {3}", "{0} — ID: {1} — {2} — /cmute {3}" } }, { "whisper.replyusage", new string[2] { "Usage: /r message (maximum 400 characters).", "Uso: /r mensaje (máximo 400 caracteres)." } }, { "whisper.noreply", new string[2] { "You have not received a whisper on this connection yet.", "Todavía no recibiste un susurro en esta conexión." } }, { "whisper.usage", new string[2] { "Usage: /w \"Player name\" message (maximum 400 characters).", "Uso: /w \"Nombre del jugador\" mensaje (máximo 400 caracteres)." } }, { "whisper.target", new string[2] { "Recipient unavailable or ambiguous. Use the full name or an ID from /cplayers.", "Destinatario ausente o ambiguo. Usá el nombre completo o el ID de /cplayers." } }, { "whisper.offline", new string[2] { "Not connected to Valheim Chat Plus. Whisper not sent.", "Sin conexión a Valheim Chat Plus. No se envió el susurro." } }, { "whisper.muted", new string[2] { "You muted this player. Unmute with /cmute before whispering.", "Tenés silenciado a ese jugador. Desbloquealo con /cmute antes de escribirle." } }, { "whisper.send", new string[2] { "Could not confirm whisper submission.", "No se pudo confirmar el envío del susurro." } }, { "label.server", new string[2] { "Server", "Servidor" } }, { "label.whisper", new string[2] { "Whisper", "Susurro" } }, { "presence.join", new string[2] { "{0} joined the server.", "{0} se conectó." } }, { "presence.leave", new string[2] { "{0} left the server.", "{0} se desconectó." } }, { "sleep", new string[2] { "{0} went to bed. ({1}/{2} players sleeping)", "{0} se acostó. ({1}/{2} jugadores durmiendo)" } }, { "help.1", new string[2] { "Normal text uses your active chat mode. Global messages reach all connected mod users.", "El texto normal usa el modo activo. El global llega a todos los usuarios conectados con el mod." } }, { "help.2", new string[2] { "/chatmode [global|vanilla|status] — switch chat mode; other features stay enabled.", "/chatmode [global|vanilla|status] — cambiar el modo; las demás funciones siguen activas." } }, { "help.3", new string[2] { "/c message — send global; /w \"Name\" message — whisper; /r message — reply.", "/c mensaje — enviar global; /w \"Nombre\" mensaje — susurrar; /r mensaje — responder." } }, { "help.4", new string[2] { "Press Tab after /w to complete a player name. Use quotes for names with spaces.", "Presioná Tab después de /w para completar un nombre. Usá comillas para nombres con espacios." } }, { "help.5", new string[2] { "/cplayers — list players and IDs; /cmute ID — mute/unmute mod messages for this connection.", "/cplayers — listar jugadores e IDs; /cmute ID — silenciar/desbloquear mensajes del mod durante esta conexión." } }, { "help.6", new string[2] { "Admins: /chatcolor set \"Name\" #RRGGBB | /chatcolor reset \"Name\". Stable IDs also work.", "Admins: /chatcolor set \"Nombre\" #RRGGBB | /chatcolor reset \"Nombre\". También admite IDs estables." } }, { "help.7", new string[2] { "Pings and join/leave, death and sleep notices stay enabled in both modes.", "Los pings y avisos de conexión, desconexión, muerte y descanso siguen activos en ambos modos." } }, { "death.0", new string[2] { "died.", "murió." } }, { "death.3", new string[2] { "died from a fall.", "murió por una caída." } }, { "death.4", new string[2] { "drowned.", "se ahogó." } }, { "death.5", new string[2] { "burned to death.", "murió quemado." } }, { "death.6", new string[2] { "froze to death.", "murió congelado." } }, { "death.7", new string[2] { "died from poison.", "murió envenenado." } }, { "death.8", new string[2] { "died in the water.", "murió en el agua." } }, { "death.9", new string[2] { "died from smoke inhalation.", "murió por inhalación de humo." } }, { "death.10", new string[2] { "fell off the edge of the world.", "cayó del borde del mundo." } }, { "death.11", new string[2] { "died from an impact.", "murió por un impacto." } }, { "death.12", new string[2] { "died in a cart accident.", "murió en un accidente con un carro." } }, { "death.13", new string[2] { "was crushed by a tree.", "murió aplastado por un árbol." } }, { "death.14", new string[2] { "died from self-inflicted damage.", "murió por daño propio." } }, { "death.15", new string[2] { "died from structural damage.", "murió por daño estructural." } }, { "death.16", new string[2] { "was killed by a turret.", "murió por una torreta." } }, { "death.17", new string[2] { "died in a boat accident.", "murió en un accidente con una embarcación." } }, { "death.18", new string[2] { "was impaled by a stalactite.", "fue empalado por una estalactita." } }, { "death.19", new string[2] { "was killed by a catapult impact.", "murió por un impacto de catapulta." } }, { "death.20", new string[2] { "died in cinder fire.", "murió por fuego de ceniza." } }, { "death.21", new string[2] { "died in the Ashlands ocean.", "murió en el océano de Ashlands." } }, { "death.22", new string[2] { "died in lava.", "murió en la lava." } }, { "death.23", new string[2] { "died in the incinerator.", "murió en el incinerador." } }, { "death.24", new string[2] { "was crushed by a drawbridge.", "murió aplastado por un puente levadizo." } }, { "death.1", new string[2] { "was killed by an enemy.", "murió a manos de un enemigo." } }, { "death.1.named", new string[2] { "was killed by {0}.", "murió a manos de {0}." } }, { "death.2", new string[2] { "died in PvP.", "murió en PvP." } }, { "death.2.named", new string[2] { "was killed by {0} (PvP).", "murió a manos de {0} (PvP)." } } }; internal static IEnumerable<string> Keys => Entries.Keys; internal static bool Has(string key) { if (key != null) { return Entries.ContainsKey(key); } return false; } internal static bool IsSpanish(string language) { if (string.IsNullOrWhiteSpace(language)) { return false; } language = language.Trim(); if (!language.StartsWith("Spanish", StringComparison.OrdinalIgnoreCase) && !language.StartsWith("Espa", StringComparison.OrdinalIgnoreCase) && !language.Equals("es", StringComparison.OrdinalIgnoreCase) && !language.StartsWith("es-", StringComparison.OrdinalIgnoreCase)) { return language.StartsWith("es_", StringComparison.OrdinalIgnoreCase); } return true; } internal static string Get(string key, bool spanish, params object[] args) { if (!Entries.TryGetValue(key, out var value)) { value = Entries["action.error"]; } string text = value[spanish ? 1u : 0u]; if (string.IsNullOrEmpty(text)) { text = value[0]; } return string.Format(CultureInfo.InvariantCulture, text, args); } } internal static class NoticePacket { internal static string Pack(string key, params string[] args) { if (!Texts.Has(key) || args.Length > 4) { throw new ArgumentException("Invalid notice key/arguments."); } string text = key; foreach (string s in args) { text = text + "\n" + Rules.Clean(s, 160); } return text; } internal static bool TryFormat(string packet, bool spanish, out string text) { text = null; if (packet == null || packet.Length > 800) { return false; } string[] array = packet.Split('\n'); if (array.Length > 5 || !Texts.Has(array[0])) { return false; } object[] array2 = new object[array.Length - 1]; for (int i = 1; i < array.Length; i++) { string text2 = Rules.Clean(array[i], 160); if (text2 != array[i]) { return false; } array2[i - 1] = text2; } try { text = Texts.Get(array[0], spanish, array2); return true; } catch (FormatException) { return false; } } } internal static class DeathName { internal static string Pack(string name, bool token) { return (token ? "K:" : "N:") + Rules.Clean(name, 78); } internal static string Display(string value, Func<string, string> localize) { if (string.IsNullOrEmpty(value)) { return ""; } if (value.StartsWith("N:", StringComparison.Ordinal)) { return Rules.Clean(value.Substring(2), 78); } if (value.StartsWith("K:", StringComparison.Ordinal)) { string text = value.Substring(2); if (!text.StartsWith("$", StringComparison.Ordinal)) { return ""; } try { string text2 = Rules.Clean(localize(text), 80); return (text2.Length == 0 || text2 == text || text2.IndexOf('$') >= 0 || text2[0] == '[') ? "" : text2; } catch { return ""; } } return ""; } } internal enum InputRoute { Vanilla, Command, Global } internal static class Rules { internal static InputRoute Route(string text, bool global) { if (string.IsNullOrWhiteSpace(text)) { return InputRoute.Vanilla; } if (WhisperRules.IsCommand(text) || ReplyState.IsCommand(text)) { return InputRoute.Command; } string text2 = text.TrimStart(); string[] array = new string[6] { "/chathelp", "/chatcolor", "/chatmode", "/cplayers", "/cmute", "/c" }; foreach (string text3 in array) { if (text2.Equals(text3, StringComparison.OrdinalIgnoreCase) || text2.StartsWith(text3 + " ", StringComparison.OrdinalIgnoreCase)) { return InputRoute.Command; } } if (text2.StartsWith("/", StringComparison.Ordinal)) { return InputRoute.Vanilla; } if (!global) { return InputRoute.Vanilla; } return InputRoute.Global; } internal static InputRoute ResolveAvailability(InputRoute route, bool available) { if (route != InputRoute.Global || available) { return route; } return InputRoute.Vanilla; } internal static string Clean(string s, int limit) { if (s == null) { return ""; } StringBuilder stringBuilder = new StringBuilder(); foreach (char c in s) { if (stringBuilder.Length >= limit) { break; } if (!char.IsControl(c) && !char.IsSurrogate(c)) { stringBuilder.Append(c switch { '>' => '›', '<' => '‹', _ => c, }); } } return stringBuilder.ToString().Trim(); } } internal sealed class Rate { private double tokens = 5.0; private double previous; internal bool Allow(double now) { tokens = Math.Min(5.0, tokens + Math.Max(0.0, now - previous)); previous = now; if (tokens < 1.0) { return false; } tokens -= 1.0; return true; } } internal static class ColorRules { internal static bool Identity(string id) { if (string.IsNullOrEmpty(id) || id.Length > 160 || !id.Contains("_")) { return false; } foreach (char c in id) { if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' && c != '-' && c != '.') { return false; } } return true; } internal static string TransportIdentity(string socketType, string host) { if (socketType == "ZSteamSocket" && ulong.TryParse(host, NumberStyles.None, CultureInfo.InvariantCulture, out var result)) { host = "Steam_" + result.ToString(CultureInfo.InvariantCulture); } if (!Identity(host)) { throw new FormatException("Stable transport identity unavailable; no character-name/UID fallback."); } return host; } internal static bool Color(string value) { if (value == null || value.Length != 7 || value[0] != '#') { return false; } for (int i = 1; i < 7; i++) { if (!Uri.IsHexDigit(value[i])) { return false; } } return true; } } internal sealed class ColorStore { private const string Header = "CDC_COLORS_1"; private static readonly string[] Palette = new string[12] { "#7DD3FC", "#FDE68A", "#C4B5FD", "#86EFAC", "#FDA4AF", "#FDBA74", "#67E8F9", "#F0ABFC", "#BEF264", "#A5B4FC", "#F9A8D4", "#D6D3D1" }; private readonly string path; private readonly Dictionary<string, string> entries = new Dictionary<string, string>(StringComparer.Ordinal); private ColorStore(string path) { this.path = path; } internal static ColorStore Load(string path) { ColorStore colorStore = new ColorStore(path); if (!File.Exists(path)) { if (Directory.Exists(path)) { throw new IOException("Color file path is a directory."); } return colorStore; } using StreamReader streamReader = new StreamReader(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), detectEncodingFromByteOrderMarks: true); if (streamReader.ReadLine() != "CDC_COLORS_1") { throw new FormatException("Invalid colors header; original file preserved."); } string text; while ((text = streamReader.ReadLine()) != null) { string[] array = text.Split('\t'); if (array.Length != 2 || !ColorRules.Identity(array[0]) || !ColorRules.Color(array[1]) || colorStore.entries.ContainsKey(array[0])) { throw new FormatException("Invalid/duplicate color entry; original file preserved."); } colorStore.entries.Add(array[0], array[1].ToUpperInvariant()); } return colorStore; } internal string GetOrAssign(string id) { if (!ColorRules.Identity(id)) { throw new FormatException("Invalid transport identity."); } if (entries.TryGetValue(id, out var value)) { return value; } HashSet<string> used = new HashSet<string>(entries.Values, StringComparer.OrdinalIgnoreCase); string text = Palette.FirstOrDefault((string item) => !used.Contains(item)); if (text == null) { uint num = 2166136261u; foreach (char c in id) { num = (num ^ c) * 16777619; } for (uint num3 = 0u; num3 < 100000; num3++) { uint num4 = num + (uint)((int)num3 * -1640531535); string text2 = "#" + ((num4 & 0x7F) + 96).ToString("X2") + (((num4 >> 8) & 0x7F) + 96).ToString("X2") + (((num4 >> 16) & 0x7F) + 96).ToString("X2"); if (!used.Contains(text2)) { text = text2; break; } } if (text == null) { throw new InvalidOperationException("color.exhausted"); } } entries.Add(id, text); try { Save(); return text; } catch { entries.Remove(id); throw; } } internal string AdminCommand(bool authorized, string command, out string target, Func<string, string> resolveName = null) { target = null; if (!authorized) { throw new UnauthorizedAccessException(); } if (command == null || command.Length > 240) { throw new FormatException("color.usage"); } string[] array = NameTargets.Words(command); if (array.Length < 3 || !array[0].Equals("/chatcolor", StringComparison.OrdinalIgnoreCase) || ((!array[1].Equals("set", StringComparison.OrdinalIgnoreCase) || array.Length != 4) && (!array[1].Equals("reset", StringComparison.OrdinalIgnoreCase) || array.Length != 3))) { throw new FormatException("color.usage"); } target = array[2]; if (!entries.ContainsKey(target) && resolveName != null) { target = resolveName(target); } if (!ColorRules.Identity(target) || !entries.TryGetValue(target, out var value)) { throw new InvalidOperationException("color.unknown"); } if (array.Length == 3) { entries.Remove(target); try { return GetOrAssign(target); } catch { entries[target] = value; throw; } } if (!ColorRules.Color(array[3])) { throw new FormatException("color.invalid"); } string color = array[3].ToUpperInvariant(); string targetId = target; if (entries.Any((KeyValuePair<string, string> p) => p.Key != targetId && p.Value.Equals(color, StringComparison.OrdinalIgnoreCase))) { throw new InvalidOperationException("color.used"); } if (color == value) { return value; } entries[target] = color; try { Save(); } catch { entries[target] = value; throw; } return color; } private void Save() { Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); string sourceFileName = path + "." + Guid.NewGuid().ToString("N") + ".tmp"; try { using (FileStream fileStream = new FileStream(sourceFileName, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { using (StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 1024, leaveOpen: true)) { streamWriter.WriteLine("CDC_COLORS_1"); foreach (KeyValuePair<string, string> item in entries.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> p) => p.Key, StringComparer.Ordinal)) { streamWriter.WriteLine(item.Key + "\t" + item.Value); } streamWriter.Flush(); } fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(sourceFileName, path, path + ".bak"); } else { File.Move(sourceFileName, path); } } finally { try { if (File.Exists(sourceFileName)) { File.Delete(sourceFileName); } } catch { } } } } internal static class ColorPacket { internal static string Pack(string id, string name, string color, string text) { return id + "\n" + name + "\n" + color + "\n" + text; } internal static bool TryRead(string packet, out string id, out string name, out string color, out string text) { id = (name = (color = (text = null))); if (packet == null || packet.Length > 520) { return false; } string[] array = packet.Split(new char[1] { '\n' }, 4); if (array.Length != 4 || !long.TryParse(array[0], out var _) || array[1].Length > 64 || !ColorRules.Color(array[2]) || array[3].Length > 400) { return false; } id = array[0]; name = Rules.Clean(array[1], 64); color = array[2]; text = Rules.Clean(array[3], 400); return text.Length != 0; } } internal static class NameTargets { internal static string Resolve(string name, IEnumerable<KeyValuePair<string, string>> players) { string text = null; foreach (KeyValuePair<string, string> player in players) { if (player.Value.Equals(name, StringComparison.OrdinalIgnoreCase)) { if (text != null) { throw new InvalidOperationException("name.duplicate"); } text = player.Key; } } if (text == null) { throw new InvalidOperationException("name.missing"); } return text; } internal static string[] Words(string text) { List<string> list = new List<string>(); int num = 0; while (num < text.Length) { if (char.IsWhiteSpace(text[num])) { num++; continue; } StringBuilder stringBuilder = new StringBuilder(); if (text[num] == '"') { num++; bool flag = false; char c; for (; num < text.Length; stringBuilder.Append(c)) { c = text[num++]; switch (c) { case '"': break; case '\\': if (num < text.Length && (text[num] == '"' || text[num] == '\\')) { c = text[num++]; } continue; default: continue; } flag = true; break; } if (!flag || (num < text.Length && !char.IsWhiteSpace(text[num]))) { throw new FormatException("name.quotes"); } } else { while (num < text.Length && !char.IsWhiteSpace(text[num])) { if (text[num] == '"') { throw new FormatException("name.quote_full"); } stringBuilder.Append(text[num++]); } } if (stringBuilder.Length == 0) { throw new FormatException("name.empty"); } list.Add(stringBuilder.ToString()); } return list.ToArray(); } } internal static class PingRules { internal static string Welcome(long uid, string name) { return "0110\n" + uid.ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(name, 64); } internal static bool ReadWelcome(string s, out long uid, out string name) { uid = 0L; name = null; if (s == null || s.Length > 90) { return false; } string[] array = s.Split('\n'); if (array.Length != 3 || array[0] != "0110" || array[2].Length > 64 || !long.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { return false; } name = Rules.Clean(array[2], 64); return true; } internal static bool Coordinate(string s, out float value) { if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value) && !float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Abs(value) <= 20000f; } return false; } internal static string Pack(float x, float y, float z) { return x.ToString("R", CultureInfo.InvariantCulture) + "\n" + y.ToString("R", CultureInfo.InvariantCulture) + "\n" + z.ToString("R", CultureInfo.InvariantCulture); } internal static bool Position(string s, out float x, out float y, out float z) { x = (y = (z = 0f)); if (s == null || s.Length > 120) { return false; } string[] array = s.Split('\n'); if (array.Length == 3 && Coordinate(array[0], out x) && Coordinate(array[1], out y)) { return Coordinate(array[2], out z); } return false; } internal static string Delivery(long uid, string name, float x, float y, float z) { return uid.ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(name, 64) + "\n" + Pack(x, y, z); } internal static bool Read(string s, out long uid, out string name, out float x, out float y, out float z) { uid = 0L; name = null; x = (y = (z = 0f)); if (s == null || s.Length > 240) { return false; } string[] array = s.Split(new char[1] { '\n' }, 3); if (array.Length != 3 || !long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out uid) || array[1].Length > 64 || !Position(array[2], out x, out y, out z)) { return false; } name = Rules.Clean(array[1], 64); return true; } } internal sealed class PingRate { private double tokens = 3.0; private double previous; internal bool Allow(double now) { tokens = Math.Min(3.0, tokens + Math.Max(0.0, now - previous) * 0.5); previous = now; if (tokens < 1.0) { return false; } tokens -= 1.0; return true; } } internal sealed class WhisperPlayer { internal string Id; internal string Name; internal long Uid; internal WhisperPlayer(string id, long uid, string name) { Id = id; Uid = uid; Name = name; } } internal static class WhisperRules { internal const int MaxPlayers = 128; internal const int MaxRoster = 32768; internal static bool IsCommand(string text) { if (text == null) { return false; } string text2 = text.TrimStart(); if (text2.Length >= 2 && text2[0] == '/' && (text2[1] == 'w' || text2[1] == 'W')) { if (text2.Length != 2) { return char.IsWhiteSpace(text2[2]); } return true; } return false; } internal static bool Parse(string text, out string target, out string message) { target = (message = null); if (!IsCommand(text) || text.Length > 700) { return false; } text = text.TrimStart(); int i; for (i = 2; i < text.Length && char.IsWhiteSpace(text[i]); i++) { } if (i == text.Length) { return false; } StringBuilder stringBuilder = new StringBuilder(); if (text[i] == '"') { i++; bool flag = false; char c; for (; i < text.Length; stringBuilder.Append(c)) { c = text[i++]; switch (c) { case '"': break; case '\\': if (i < text.Length && (text[i] == '"' || text[i] == '\\')) { c = text[i++]; } continue; default: continue; } flag = true; break; } if (!flag || (i < text.Length && !char.IsWhiteSpace(text[i]))) { return false; } } else { while (i < text.Length && !char.IsWhiteSpace(text[i])) { if (text[i] == '"') { return false; } stringBuilder.Append(text[i++]); } } target = stringBuilder.ToString(); message = text.Substring(i).Trim(); if (target.Length > 0 && target.Length <= 160 && message.Length > 0) { return message.Length <= 400; } return false; } internal static string Quote(string name) { return "\"" + name.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } internal static WhisperPlayer Resolve(string target, IReadOnlyList<WhisperPlayer> players) { WhisperPlayer whisperPlayer = null; foreach (WhisperPlayer player in players) { if (player.Id == target) { if (whisperPlayer != null) { return null; } whisperPlayer = player; } } if (whisperPlayer != null) { return whisperPlayer; } foreach (WhisperPlayer player2 in players) { if (player2.Name.Equals(target, StringComparison.OrdinalIgnoreCase)) { if (whisperPlayer != null) { return null; } whisperPlayer = player2; } } return whisperPlayer; } internal static string Roster(IReadOnlyList<WhisperPlayer> players) { if (players.Count > 128) { throw new InvalidOperationException("Too many DirectChat players."); } StringBuilder stringBuilder = new StringBuilder(); foreach (WhisperPlayer player in players) { if (stringBuilder.Length > 0) { stringBuilder.Append('\n'); } stringBuilder.Append(player.Id).Append('\t').Append(player.Uid.ToString(CultureInfo.InvariantCulture)) .Append('\t') .Append(Rules.Clean(player.Name, 64)); } if (stringBuilder.Length > 32768) { throw new InvalidOperationException("Roster too large."); } return stringBuilder.ToString(); } internal static bool ReadRoster(string packet, out WhisperPlayer[] players) { players = null; if (packet == null || packet.Length > 32768) { return false; } if (packet.Length == 0) { players = Array.Empty<WhisperPlayer>(); return true; } string[] array = packet.Split('\n'); if (array.Length > 128) { return false; } List<WhisperPlayer> list = new List<WhisperPlayer>(); HashSet<string> hashSet = new HashSet<string>(); HashSet<long> hashSet2 = new HashSet<long>(); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string[] array3 = array2[i].Split('\t'); if (array3.Length != 3 || array3[0].Length == 0 || array3[0].Length > 160 || Rules.Clean(array3[0], 160) != array3[0] || array3[2].Length > 64 || !long.TryParse(array3[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !hashSet.Add(array3[0]) || !hashSet2.Add(result)) { return false; } list.Add(new WhisperPlayer(array3[0], result, Rules.Clean(array3[2], 64))); } players = list.ToArray(); return true; } internal static string Submit(string id, string message) { return id + "\t" + Rules.Clean(message, 400); } internal static bool ReadSubmit(string packet, out string id, out string message) { id = (message = null); if (packet == null || packet.Length > 561) { return false; } string[] array = packet.Split('\t'); if (array.Length != 2 || array[0].Length == 0 || array[0].Length > 160 || array[1].Length > 400) { return false; } id = array[0]; message = Rules.Clean(array[1], 400); return message.Length > 0; } internal static string Delivery(WhisperPlayer from, WhisperPlayer to, string message) { return from.Uid.ToString(CultureInfo.InvariantCulture) + "\t" + Rules.Clean(from.Name, 64) + "\t" + to.Uid.ToString(CultureInfo.InvariantCulture) + "\t" + Rules.Clean(to.Name, 64) + "\t" + Rules.Clean(message, 400) + "\t" + from.Id; } internal static bool ReadDelivery(string packet, long ownUid, out long from, out string fromName, out long to, out string toName, out string message) { string senderId; return ReadDelivery(packet, ownUid, out from, out fromName, out to, out toName, out message, out senderId); } internal static bool ReadDelivery(string packet, long ownUid, out long from, out string fromName, out long to, out string toName, out string message, out string senderId) { senderId = null; from = (to = 0L); fromName = (toName = (message = null)); if (packet == null || packet.Length > 741) { return false; } string[] array = packet.Split('\t'); if (array.Length != 6 || !ColorRules.Identity(array[5]) || array[1].Length > 64 || array[3].Length > 64 || array[4].Length > 400 || !long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out from) || !long.TryParse(array[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out to) || (ownUid != from && ownUid != to)) { return false; } senderId = array[5]; fromName = Rules.Clean(array[1], 64); toName = Rules.Clean(array[3], 64); message = Rules.Clean(array[4], 400); return message.Length > 0; } internal static void Deliver<T>(T sender, T target, string packet, Action<T, string> send) where T : class { send(target, packet); if (sender != target) { send(sender, packet); } } } internal sealed class WhisperCompletion { private string last; private string prefix; private string suffix; private int lastCaret; private int index; private string[] matches; internal void Reset() { last = (prefix = (suffix = null)); matches = null; index = (lastCaret = 0); } internal bool Complete(string text, int caret, IReadOnlyList<WhisperPlayer> roster, out string result, out int position) { result = text; position = caret; if (text == last && caret == lastCaret && matches != null) { index = (index + 1) % matches.Length; } else { Reset(); if (!WhisperRules.IsCommand(text) || text.TrimStart().Length < 3 || caret < 0 || caret > text.Length) { return false; } int i; for (i = text.Length - text.TrimStart().Length + 2; i < text.Length && char.IsWhiteSpace(text[i]); i++) { } if (caret < i) { return false; } string text2 = text.Substring(i, caret - i); string value = text2; if (text2.StartsWith("\"", StringComparison.Ordinal)) { StringBuilder stringBuilder = new StringBuilder(); for (int j = 1; j < text2.Length; j++) { char c = text2[j]; switch (c) { case '"': return false; case '\\': if (j + 1 < text2.Length && (text2[j + 1] == '"' || text2[j + 1] == '\\')) { c = text2[++j]; } break; } stringBuilder.Append(c); } value = stringBuilder.ToString(); } else { string text3 = text2; foreach (char c2 in text3) { if (char.IsWhiteSpace(c2) || c2 == '"') { return false; } } } if (caret < text.Length && !char.IsWhiteSpace(text[caret])) { return false; } List<string> list = new List<string>(); foreach (WhisperPlayer item in roster) { if (item.Name.Length != 0 && item.Name.StartsWith(value, StringComparison.OrdinalIgnoreCase)) { list.Add((WhisperRules.Resolve(item.Name, roster) == item) ? WhisperRules.Quote(item.Name) : item.Id); } } if (list.Count == 0) { return false; } list.Sort(StringComparer.OrdinalIgnoreCase); matches = list.ToArray(); prefix = text.Substring(0, i); suffix = text.Substring(caret); } string text4 = matches[index]; result = prefix + text4 + ((suffix.Length == 0) ? " " : suffix); position = prefix.Length + text4.Length + ((suffix.Length == 0) ? 1 : 0); last = result; lastCaret = position; return true; } } internal enum PlayerEventKind { None, Sleep, Death } internal static class PlayerEventRules { internal const int MaxHitType = 24; internal static string SleepSubmit(bool sleeping) { return "S\n" + (sleeping ? "1" : "0"); } internal static string DeathSubmit(int hitType, string attacker) { return "D\n" + NormalizeHitType(hitType).ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(attacker, 80); } internal static bool ReadSubmit(string packet, out PlayerEventKind kind, out bool sleeping, out int hitType, out string attacker) { kind = PlayerEventKind.None; sleeping = false; hitType = 0; attacker = ""; if (packet == null || packet.Length > 128) { return false; } string[] array = packet.Split('\n'); if (array.Length == 2 && array[0] == "S" && (array[1] == "0" || array[1] == "1")) { kind = PlayerEventKind.Sleep; sleeping = array[1] == "1"; return true; } if (array.Length == 3 && array[0] == "D" && int.TryParse(array[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result) && result >= 0 && result <= 24) { string text = Rules.Clean(array[2], 80); if (text != array[2]) { return false; } kind = PlayerEventKind.Death; hitType = result; attacker = text; return true; } return false; } internal static string SleepDelivery(string name, int sleeping, int total) { return "S\n" + Rules.Clean(name, 64) + "\n" + Math.Max(0, sleeping).ToString(CultureInfo.InvariantCulture) + "\n" + Math.Max(0, total).ToString(CultureInfo.InvariantCulture); } internal static string DeathDelivery(string victim, int hitType, string attacker) { return "D\n" + Rules.Clean(victim, 64) + "\n" + NormalizeHitType(hitType).ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(attacker, 80); } internal static bool ReadDelivery(string packet, out PlayerEventKind kind, out string name, out int sleeping, out int total, out int hitType, out string attacker) { kind = PlayerEventKind.None; name = ""; sleeping = (total = (hitType = 0)); attacker = ""; if (packet == null || packet.Length > 240) { return false; } string[] array = packet.Split('\n'); if (array.Length == 4 && array[0] == "S") { string text = Rules.Clean(array[1], 64); if (text.Length == 0 || text != array[1] || !int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out sleeping) || !int.TryParse(array[3], NumberStyles.None, CultureInfo.InvariantCulture, out total) || sleeping < 0 || total < 1 || sleeping > total || total > 1000) { return false; } kind = PlayerEventKind.Sleep; name = text; return true; } if (array.Length == 4 && array[0] == "D") { string text2 = Rules.Clean(array[1], 64); string text3 = Rules.Clean(array[3], 80); if (text2.Length == 0 || text2 != array[1] || text3 != array[3] || !int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out hitType) || hitType < 0 || hitType > 24) { return false; } kind = PlayerEventKind.Death; name = text2; attacker = text3; return true; } return false; } internal static string SleepLine(string name, int sleeping, int total, bool spanish) { return "<color=#B8C7FF>[" + Texts.Get("label.server", spanish) + "] " + Texts.Get("sleep", spanish, Rules.Clean(name, 64), sleeping, total) + "</color>"; } internal static string De