Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of WhatsDab v1.1.0
WhatsDab.dll
Decompiled 2 months agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Networking; using Il2CppScheduleOne.PlayerScripts; using Il2CppSteamworks; using Il2CppSystem.Collections.Generic; using MelonLoader; using Microsoft.CodeAnalysis; using Sideload.Api; using UnityEngine; using WhatsDab; using WhatsDab.Chat; using WhatsDab.Lobby; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "WhatsDab", "1.1.0", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-WhatsDab")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: MelonOptionalDependencies(new string[] { "Sideload" })] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("WhatsDab")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0+642e8fea8bea44ef6dd6d70b9ca4c21cbf40e4f2")] [assembly: AssemblyProduct("WhatsDab")] [assembly: AssemblyTitle("WhatsDab")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: AssemblyVersion("1.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace DooDesch { internal static class ModVersion { internal const string Current = "1.1.0"; } } namespace Sideload.Api { public static class Apps { private static bool _bound; private static int _probeAttempts; private static readonly List<Action> _pending = new List<Action>(); private static Action<string, string, string, string, Assembly> _registerApp; private static Action<string, string, Func<string, string, string>> _handle; private static Action<string, string, string> _emit; private static Action<string, string> _allowHost; private static Action<string, string> _declareOrientations; private static Action<string, int> _setBadge; private static Action<string, string, string> _notify; private static Action<string, string, string, float> _notifyFor; private static Func<string, bool> _isOnScreen; private static Action<string, string, byte[]> _setImage; private static Action<string, bool> _setIconHidden; private static Action<string, bool> _setAppOpen; private static Func<string, bool> _isAppOpen; private static Func<bool, bool> _setPhoneRaised; private static Func<bool> _isPhoneRaised; private static Action<string, string, Func<string, string, bool>> _claimKeys; public static bool Available { get { EnsureBound(); return _bound; } } internal static bool HasKeys { get { EnsureBound(); return _claimKeys != null; } } internal static bool HasPhone { get { EnsureBound(); return _setPhoneRaised != null; } } internal static bool HasOpen { get { EnsureBound(); if (_setAppOpen != null) { return _setIconHidden != null; } return false; } } public static AppHandle Register(string id, string bundlePrefix, string title = null, string iconLabel = null, Assembly hostAssembly = null) { AppHandle result = new AppHandle(id); if (string.IsNullOrEmpty(id)) { return result; } Assembly asm = hostAssembly ?? Assembly.GetCallingAssembly(); string t = title; string il = iconLabel; string prefix = bundlePrefix; EnsureBound(); if (_registerApp != null) { _registerApp(id, t, il, prefix, asm); } else { _pending.Add(delegate { _registerApp?.Invoke(id, t, il, prefix, asm); }); } return result; } internal static void WhenBound(Action work) { if (work != null) { EnsureBound(); if (_bound) { work(); } else { _pending.Add(work); } } } internal static void HandleCall(string appId, string name, Func<string, string> handler) { if (_handle != null && handler != null) { _handle(appId, name, (string app, string argument) => handler(argument)); } } internal static void EmitEvent(string appId, string name, string payload) { _emit?.Invoke(appId, name, payload); } internal static void AllowNetHost(string appId, string host) { _allowHost?.Invoke(appId, host); } internal static void Orient(string appId, string orientations) { _declareOrientations?.Invoke(appId, orientations); } internal static void Badge(string appId, int count) { _setBadge?.Invoke(appId, count); } internal static void Notify(string appId, string title, string subtitle, float seconds) { if (_notifyFor != null) { _notifyFor(appId, title, subtitle, seconds); } else if (_notify != null) { _notify(appId, title, subtitle); } } internal static bool OnScreen(string appId) { if (_isOnScreen != null) { return _isOnScreen(appId); } return false; } internal static void SetImage(string appId, string name, byte[] png) { _setImage?.Invoke(appId, name, png); } internal static void HideIcon(string appId, bool hidden) { _setIconHidden?.Invoke(appId, hidden); } internal static void OpenApp(string appId, bool open) { _setAppOpen?.Invoke(appId, open); } internal static bool AppIsOpen(string appId) { if (_isAppOpen != null) { return _isAppOpen(appId); } return false; } internal static bool RaisePhone(bool raised) { if (_setPhoneRaised != null) { return _setPhoneRaised(raised); } return false; } internal static bool PhoneIsRaised() { if (_isPhoneRaised != null) { return _isPhoneRaised(); } return false; } internal static void ClaimKeys(string appId, string keys, Func<string, string, bool> handler) { _claimKeys?.Invoke(appId, keys, handler); } private static void EnsureBound() { if (_bound) { return; } try { Type type = FindBridge(_probeAttempts++ % 30 == 0); if (type == null || (type.GetField("AbiVersion", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) is int num && num < 1)) { return; } _registerApp = Get<Action<string, string, string, string, Assembly>>(type, "RegisterApp"); if (_registerApp == null) { return; } _handle = Get<Action<string, string, Func<string, string, string>>>(type, "Handle"); _emit = Get<Action<string, string, string>>(type, "Emit"); _allowHost = Get<Action<string, string>>(type, "AllowHost"); _declareOrientations = Get<Action<string, string>>(type, "DeclareOrientations"); _setBadge = Get<Action<string, int>>(type, "SetBadge"); _notify = Get<Action<string, string, string>>(type, "Notify"); _notifyFor = Get<Action<string, string, string, float>>(type, "NotifyFor"); _isOnScreen = Get<Func<string, bool>>(type, "IsAppOnScreen"); _setImage = Get<Action<string, string, byte[]>>(type, "SetImage"); _setIconHidden = Get<Action<string, bool>>(type, "SetIconHidden"); _setAppOpen = Get<Action<string, bool>>(type, "SetAppOpen"); _isAppOpen = Get<Func<string, bool>>(type, "IsAppOpen"); _setPhoneRaised = Get<Func<bool, bool>>(type, "SetPhoneRaised"); _isPhoneRaised = Get<Func<bool>>(type, "IsPhoneRaised"); _claimKeys = Get<Action<string, string, Func<string, string, bool>>>(type, "ClaimKeys"); _bound = true; for (int i = 0; i < _pending.Count; i++) { try { _pending[i](); } catch { } } _pending.Clear(); } catch { } } private static T Get<T>(Type t, string field) where T : class { return t.GetField(field, BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as T; } private static Type FindBridge(bool scan) { Type type = Type.GetType("Sideload.Bridge.SideloadBridge, Sideload", throwOnError: false); if (type != null || !scan) { return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetType("Sideload.Bridge.SideloadBridge", throwOnError: false); if (type != null) { return type; } } catch { } } return null; } } public sealed class AppHandle { private readonly string _id; public string Id => _id; public bool IsOnScreen { get { if (Apps.Available) { return Apps.OnScreen(_id); } return false; } } public bool IsOpen { get { if (Apps.Available) { return Apps.AppIsOpen(_id); } return false; } } public static bool CanOpenProgrammatically { get { if (Apps.Available) { return Apps.HasOpen; } return false; } } public static bool CanClaimKeys { get { if (Apps.Available) { return Apps.HasKeys; } return false; } } internal AppHandle(string id) { _id = id ?? ""; } public AppHandle OnCall(string name, Func<string, string> handler) { string id = _id; Apps.WhenBound(delegate { Apps.HandleCall(id, name, handler); }); return this; } public void Emit(string name, string payload = "") { string id = _id; Apps.WhenBound(delegate { Apps.EmitEvent(id, name, payload); }); } public AppHandle AllowHost(string host) { string id = _id; Apps.WhenBound(delegate { Apps.AllowNetHost(id, host); }); return this; } public AppHandle Orientation(params string[] supported) { string id = _id; string list = ((supported == null) ? "" : string.Join(",", supported)); Apps.WhenBound(delegate { Apps.Orient(id, list); }); return this; } public AppHandle Badge(int count) { string id = _id; Apps.WhenBound(delegate { Apps.Badge(id, count); }); return this; } public AppHandle Notify(string title, string subtitle = "", float seconds = 0f) { string id = _id; Apps.WhenBound(delegate { Apps.Notify(id, title, subtitle, seconds); }); return this; } public AppHandle Image(string name, byte[] png) { string id = _id; Apps.WhenBound(delegate { Apps.SetImage(id, name, png); }); return this; } public AppHandle Icon(bool visible) { Apps.HideIcon(_id, !visible); return this; } public AppHandle NoIcon() { string id = _id; Apps.WhenBound(delegate { Apps.HideIcon(id, hidden: true); }); return this; } public AppHandle Open() { string id = _id; Apps.WhenBound(delegate { Apps.OpenApp(id, open: true); }); return this; } public AppHandle Close() { string id = _id; Apps.WhenBound(delegate { Apps.OpenApp(id, open: false); }); return this; } public bool Show() { if (!Apps.RaisePhone(raised: true)) { return false; } Apps.OpenApp(_id, open: true); return true; } public AppHandle OnKey(string keys, Func<string, bool> handler) { string id = _id; Apps.WhenBound(delegate { Apps.ClaimKeys(id, keys, (handler == null) ? null : ((Func<string, string, bool>)((string _, string key) => handler(key)))); }); return this; } public AppHandle Hide() { string id = _id; Apps.WhenBound(delegate { Apps.OpenApp(id, open: false); Apps.RaisePhone(raised: false); }); return this; } } public static class PhoneScreen { public static bool IsRaised { get { if (Apps.Available) { return Apps.PhoneIsRaised(); } return false; } } public static bool Available { get { if (Apps.Available) { return Apps.HasPhone; } return false; } } public static bool Raise() { return Apps.RaisePhone(raised: true); } public static bool Lower() { return Apps.RaisePhone(raised: false); } } } namespace WhatsDab { public class Core : MelonMod { internal static Instance Log; public override void OnInitializeMelon() { Log = ((MelonBase)this).LoggerInstance; ChatBackend.Install(Apps.Register("whatsdab", "WhatsDab.Assets.whatsdab", "WhatsDab", "WhatsDab").Orientation("landscape", "portrait"), ChooseSource()); Log.Msg(Apps.Available ? "[WhatsDab] registered with Sideload." : "[WhatsDab] Sideload is not loaded yet - registration is queued and replays when it appears."); } private static IChatSource ChooseSource() { return new LobbySource(); } public override void OnUpdate() { ChatBackend.Tick(); } } } namespace WhatsDab.Lobby { internal sealed class ChatMessage { public ulong SenderId; public ulong RecipientId; public int Seq; public long UnixSeconds; public string Text = ""; public bool Mine; public bool Pending; } internal static class ChatEnvelope { private const string Prefix = "WD1|MSG|"; internal const int MaxTextChars = 500; internal static bool IsOurs(string raw) { return raw?.StartsWith("WD1|MSG|", StringComparison.Ordinal) ?? false; } internal static byte[] Encode(ulong recipientId, int seq, long unix, string text) { string text2 = text ?? ""; if (text2.Length > 500) { int num = 500; if (char.IsHighSurrogate(text2[num - 1])) { num--; } text2 = text2.Substring(0, num); } string text3 = ((recipientId == 0L) ? "*" : recipientId.ToString()); string text4 = Convert.ToBase64String(Encoding.UTF8.GetBytes(text2)); string s = "WD1|MSG|" + text3 + "|" + seq + "|" + unix + "|" + text4 + "\0"; return Encoding.ASCII.GetBytes(s); } internal static ChatMessage Decode(string raw, ulong senderId) { if (!IsOurs(raw)) { return null; } try { string[] array = raw.Split('|'); if (array.Length != 6) { return null; } ChatMessage chatMessage = new ChatMessage { SenderId = senderId }; chatMessage.RecipientId = ((array[2] == "*") ? 0 : (ulong.TryParse(array[2], out var result) ? result : 0)); int.TryParse(array[3], out chatMessage.Seq); long.TryParse(array[4], out chatMessage.UnixSeconds); chatMessage.Text = Encoding.UTF8.GetString(Convert.FromBase64String(array[5])); return chatMessage; } catch { return null; } } } internal static class ChatStore { internal const ulong GroupKey = 0uL; internal const ulong NoThread = ulong.MaxValue; private static readonly Dictionary<ulong, List<ChatMessage>> _threads = new Dictionary<ulong, List<ChatMessage>>(); private static readonly Dictionary<ulong, int> _unread = new Dictionary<ulong, int>(); private const int MaxMessagesPerThread = 200; internal static int Revision { get; private set; } internal static IReadOnlyList<ChatMessage> Thread(ulong peerKey) { if (!_threads.TryGetValue(peerKey, out var value)) { return Array.Empty<ChatMessage>(); } return value; } internal static int Unread(ulong peerKey) { if (!_unread.TryGetValue(peerKey, out var value)) { return 0; } return value; } internal static int TotalUnread() { int num = 0; foreach (KeyValuePair<ulong, int> item in _unread) { num += item.Value; } return num; } private static ulong KeyFor(ChatMessage m, ulong self) { if (m.RecipientId != 0L) { if (m.SenderId != self) { return m.SenderId; } return m.RecipientId; } return 0uL; } internal static ChatMessage AddLocal(ulong recipientKey, int seq, string text) { ulong senderId = ChatTransport.SelfId(); ChatMessage chatMessage = new ChatMessage { SenderId = senderId, RecipientId = recipientKey, Seq = seq, UnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Text = text, Mine = true, Pending = true }; List(recipientKey).Add(chatMessage); Revision++; return chatMessage; } internal static void Receive(ChatMessage m, ulong activeThreadKey) { ulong num = ChatTransport.SelfId(); ulong num2 = KeyFor(m, num); if (m.SenderId == num) { List<ChatMessage> list = List(num2); for (int num3 = list.Count - 1; num3 >= 0; num3--) { if (list[num3].Mine && list[num3].Pending && list[num3].Seq == m.Seq) { list[num3].Pending = false; Revision++; return; } } m.Mine = true; m.Pending = false; list.Add(m); Revision++; } else { List(num2).Add(m); if (num2 != activeThreadKey) { _unread[num2] = Unread(num2) + 1; } Revision++; } } internal static void MarkRead(ulong peerKey) { if (_unread.TryGetValue(peerKey, out var value) && value > 0) { _unread[peerKey] = 0; Revision++; } } internal static void Clear() { _threads.Clear(); _unread.Clear(); Revision++; } private static List<ChatMessage> List(ulong key) { if (!_threads.TryGetValue(key, out var value)) { value = new List<ChatMessage>(); _threads[key] = value; } if (value.Count > 200) { value.RemoveRange(0, value.Count - 200); } return value; } } internal static class ChatTransport { private const int MinSendIntervalMs = 300; private static Callback<LobbyChatMsg_t> _callback; private static Callback<LobbyEnter_t> _enterCallback; private static Callback<LobbyCreated_t> _createdCallback; private static Action<ChatMessage> _onMessage; private static long _lastSendTicks; private static ulong _seenLobby; private static bool _loggedDeadProperty; internal static bool InLobby => CurrentLobby() != 0; internal static ulong LobbyId => CurrentLobby(); internal static void Start(Action<ChatMessage> onMessage) { _onMessage = onMessage; if (_callback != null) { return; } try { _callback = Callback<LobbyChatMsg_t>.Create(DispatchDelegate<LobbyChatMsg_t>.op_Implicit((Action<LobbyChatMsg_t>)OnChatMsg)); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Error("[WhatsDab] chat callback registration failed: " + ex); } } try { _enterCallback = Callback<LobbyEnter_t>.Create(DispatchDelegate<LobbyEnter_t>.op_Implicit((Action<LobbyEnter_t>)OnLobbyEnter)); _createdCallback = Callback<LobbyCreated_t>.Create(DispatchDelegate<LobbyCreated_t>.op_Implicit((Action<LobbyCreated_t>)OnLobbyCreated)); } catch (Exception ex2) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[WhatsDab] lobby callbacks failed: " + ex2.Message); } } } private static void OnLobbyEnter(LobbyEnter_t e) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) if (e.m_EChatRoomEnterResponse == 1) { _seenLobby = e.m_ulSteamIDLobby; } } private static void OnLobbyCreated(LobbyCreated_t e) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_0009: Unknown result type (might be due to invalid IL or missing references) if ((int)e.m_eResult == 1) { _seenLobby = e.m_ulSteamIDLobby; } } internal static ulong SelfId() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) try { return SteamUser.GetSteamID().m_SteamID; } catch { return 0uL; } } private static ulong CurrentLobby() { ulong num = FromLobbyService(); if (num != 0L) { return num; } if (_seenLobby != 0L && !_loggedDeadProperty) { _loggedDeadProperty = true; Instance log = Core.Log; if (log != null) { log.Warning("[WhatsDab] the game's lobby service gave no id; using our own record " + _seenLobby + " (SteamLobbyService may have changed again)."); } } return _seenLobby; } private static ulong FromLobbyService() { try { Lobby instance = Singleton<Lobby>.Instance; if ((Object)(object)instance == (Object)null || !instance.IsInLobby) { return 0uL; } ILobbyService lobbyService = instance._lobbyService; if (lobbyService == null) { return 0uL; } SteamLobbyService val = ((Il2CppObjectBase)lobbyService).TryCast<SteamLobbyService>(); return (val != null) ? val._lobbyID : 0; } catch { return 0uL; } } internal static int Send(ulong recipientId, int seq, string text) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) ulong num = CurrentLobby(); if (num == 0L) { return -1; } long ticks = DateTime.UtcNow.Ticks; if (ticks - _lastSendTicks < 3000000) { return -1; } _lastSendTicks = ticks; try { long unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); byte[] array = ChatEnvelope.Encode(recipientId, seq, unix, text); return SteamMatchmaking.SendLobbyChatMsg(new CSteamID(num), Il2CppStructArray<byte>.op_Implicit(array), array.Length) ? seq : (-1); } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[WhatsDab] send failed: " + ex.Message); } return -1; } } private static void OnChatMsg(LobbyChatMsg_t msg) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) try { CSteamID val = new CSteamID(msg.m_ulSteamIDLobby); Il2CppStructArray<byte> val2 = new Il2CppStructArray<byte>(4096L); CSteamID val3 = default(CSteamID); EChatEntryType val4 = default(EChatEntryType); int lobbyChatEntry = SteamMatchmaking.GetLobbyChatEntry(val, (int)msg.m_iChatID, ref val3, val2, ((Il2CppArrayBase<byte>)(object)val2).Length, ref val4); int num = Math.Max(0, Math.Min(lobbyChatEntry, ((Il2CppArrayBase<byte>)(object)val2).Length)); if (num == 0) { return; } byte[] array = new byte[num]; for (int i = 0; i < num; i++) { array[i] = ((Il2CppArrayBase<byte>)(object)val2)[i]; } string raw = Encoding.ASCII.GetString(array).TrimEnd('\0'); if (ChatEnvelope.IsOurs(raw)) { ChatMessage chatMessage = ChatEnvelope.Decode(raw, val3.m_SteamID); if (chatMessage != null && (chatMessage.RecipientId == 0L || chatMessage.RecipientId == SelfId() || chatMessage.SenderId == SelfId())) { _onMessage?.Invoke(chatMessage); } } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[WhatsDab] receive failed: " + ex.Message); } } } } internal sealed class Contact { public ulong SteamId; public string Name = ""; } internal static class Contacts { private static readonly List<Contact> _list = new List<Contact>(); internal static IReadOnlyList<Contact> All => _list; internal static string NameOf(ulong steamId) { foreach (Contact item in _list) { if (item.SteamId == steamId) { return item.Name; } } return ResolveName(steamId); } internal static void Refresh(ulong lobbyId) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) _list.Clear(); if (lobbyId == 0L) { return; } ulong num = ChatTransport.SelfId(); try { CSteamID val = default(CSteamID); ((CSteamID)(ref val))..ctor(lobbyId); int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(val); for (int i = 0; i < numLobbyMembers; i++) { CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(val, i); if (lobbyMemberByIndex.m_SteamID != 0L && lobbyMemberByIndex.m_SteamID != num) { _list.Add(new Contact { SteamId = lobbyMemberByIndex.m_SteamID, Name = ResolveName(lobbyMemberByIndex.m_SteamID) }); } } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[WhatsDab] contact refresh failed: " + ex.Message); } } } private static string ResolveName(ulong steamId) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) try { string text = steamId.ToString(); List<Player> playerList = Player.PlayerList; if (playerList != null) { for (int i = 0; i < playerList.Count; i++) { Player val = playerList[i]; if ((Object)(object)val == (Object)null) { continue; } string text2 = null; try { text2 = val.PlayerCode; } catch { } if (text2 == text) { string text3 = null; try { text3 = val.PlayerName; } catch { } if (!string.IsNullOrEmpty(text3)) { return text3; } } } } } catch { } try { string friendPersonaName = SteamFriends.GetFriendPersonaName(new CSteamID(steamId)); if (!string.IsNullOrEmpty(friendPersonaName) && friendPersonaName != "[unknown]") { return friendPersonaName; } } catch { } string text4 = steamId.ToString(); return "Player " + ((text4.Length > 4) ? text4.Substring(text4.Length - 4) : text4); } } internal sealed class LobbySource : IChatSource { private const float ContactRefreshSeconds = 5f; private readonly List<Thread> _threads = new List<Thread>(); private int _threadsRevision = -1; private int _seq; private float _nextRefresh; private ulong _lobby; private Thread _arrival; public string Self => "You"; public int Revision => ChatStore.Revision; public bool Online => ChatTransport.InLobby; public int Peers => Contacts.All.Count; public string TypingIn => null; public IReadOnlyList<Thread> Threads { get { Rebuild(); return _threads; } } internal LobbySource() { ChatTransport.Start(OnIncoming); } public Thread Find(string id) { foreach (Thread thread in Threads) { if (string.Equals(thread.Id, id, StringComparison.OrdinalIgnoreCase)) { return thread; } } return null; } public bool Send(string threadId, string text) { if (!TryKey(threadId, out var key)) { return false; } int seq = ++_seq; if (ChatTransport.Send(key, seq, text) < 0) { return false; } ChatStore.AddLocal(key, seq, text); return true; } public void MarkRead(string threadId) { if (TryKey(threadId, out var key)) { ChatStore.MarkRead(key); } } public int TotalUnread() { return ChatStore.TotalUnread(); } public Thread TakeArrival() { Thread arrival = _arrival; _arrival = null; return arrival; } public void Tick() { ulong lobbyId = ChatTransport.LobbyId; if (lobbyId != _lobby) { _lobby = lobbyId; ChatStore.Clear(); Contacts.Refresh(lobbyId); _nextRefresh = 5f; } else if (lobbyId != 0L) { _nextRefresh -= Time.deltaTime; if (!(_nextRefresh > 0f)) { _nextRefresh = 5f; Contacts.Refresh(lobbyId); } } } private static bool TryKey(string threadId, out ulong key) { key = 0uL; if (string.IsNullOrWhiteSpace(threadId)) { return false; } if (string.Equals(threadId, "everyone", StringComparison.OrdinalIgnoreCase)) { return true; } return ulong.TryParse(threadId.Trim(), out key); } private void OnIncoming(ChatMessage message) { ulong activeThreadKey = ulong.MaxValue; ChatStore.Receive(message, activeThreadKey); ulong num = ChatTransport.SelfId(); ulong num2 = ((message.RecipientId == 0L) ? 0 : ((message.SenderId == num) ? message.RecipientId : message.SenderId)); _threadsRevision = -1; _arrival = Find((num2 == 0L) ? "everyone" : num2.ToString()); } private void Rebuild() { if (_threadsRevision == ChatStore.Revision) { return; } _threadsRevision = ChatStore.Revision; _threads.Clear(); _threads.Add(BuildThread("everyone", "Everyone", group: true, 0uL)); foreach (Contact item in Contacts.All) { _threads.Add(BuildThread(item.SteamId.ToString(), item.Name, group: false, item.SteamId)); } } private static Thread BuildThread(string id, string name, bool group, ulong key) { Thread thread = new Thread { Id = id, Name = name, Group = group, Unread = ChatStore.Unread(key) }; foreach (ChatMessage item in ChatStore.Thread(key)) { thread.Messages.Add(new Message { From = (item.Mine ? "You" : Contacts.NameOf(item.SenderId)), Text = item.Text, At = DateTimeOffset.FromUnixTimeSeconds(item.UnixSeconds).ToLocalTime().DateTime, Mine = item.Mine, Pending = item.Pending }); } return thread; } } } namespace WhatsDab.Chat { internal static class ChatBackend { internal const int MaxMessageChars = 500; private static AppHandle _app; private static IChatSource _source; private static int _pushedRevision = -1; internal static IChatSource Source => _source; internal static void UseSource(IChatSource source) { _source = source; _pushedRevision = -1; } internal static void Install(AppHandle app, IChatSource source) { _app = app; UseSource(source); app.OnCall("chat.threads", (string _) => Threads()).OnCall("chat.thread", Conversation).OnCall("chat.send", Send) .OnCall("chat.read", delegate(string id) { if (_app == null || _app.IsOnScreen) { _source.MarkRead(id); } return "ok"; }) .OnCall("chat.self", (string _) => _source.Self) .OnCall("chat.status", (string _) => Status()) .OnKey("Enter", (string _) => QuickChat()); } internal static bool QuickChat() { if (_app == null || _source == null || !_source.Online) { return false; } if (!_app.Show()) { return false; } _app.Emit("chat.compose", "everyone"); return true; } internal static void Tick() { if (_source != null) { _source.Tick(); if (_source.Revision != _pushedRevision) { _pushedRevision = _source.Revision; int count = _source.TotalUnread(); _app?.Emit("chat.changed", count.ToString()); _app?.Badge(count); NotifyIfUnseen(); } } } private static void NotifyIfUnseen() { Thread thread = _source.TakeArrival(); if (thread != null && _app != null && !_app.IsOnScreen) { Message message = ((thread.Messages.Count > 0) ? thread.Messages[thread.Messages.Count - 1] : null); if (message != null && !message.Mine) { _app.Notify(thread.Group ? (message.From + " - " + thread.Name) : message.From, message.Text); } } } internal static string Status() { return Json.Object().Add("online", _source.Online).Add("peers", _source.Peers) .Close(); } internal static string Threads() { Json json = Json.Array(); foreach (Thread thread in _source.Threads) { Message message = ((thread.Messages.Count > 0) ? thread.Messages[thread.Messages.Count - 1] : null); json.Item(Json.Object().Add("id", thread.Id).Add("name", thread.Name) .Add("group", thread.Group) .Add("unread", thread.Unread) .Add("typing", string.Equals(_source.TypingIn, thread.Id, StringComparison.OrdinalIgnoreCase)) .Add("preview", (message == null) ? "" : ((message.Mine ? "You: " : (thread.Group ? (message.From + ": ") : "")) + message.Text)) .Add("time", (message == null) ? "" : ChatModel.Clock(message.At))); } return json.Close(); } internal static string Conversation(string id) { Thread thread = _source.Find(id); if (thread == null) { return Json.Object().Add("id", "").Add("name", "") .Close(); } Json json = Json.Array(); string text = null; foreach (Message message in thread.Messages) { string text2 = ChatModel.DayLabel(message.At); if (text2 != text) { json.Item(Json.Object().Add("kind", "day").Add("text", text2)); text = text2; } json.Item(Json.Object().Add("kind", "message").Add("from", message.From) .Add("text", message.Text) .Add("time", ChatModel.Clock(message.At)) .Add("mine", message.Mine) .Add("pending", message.Pending)); } return Json.Object().Add("id", thread.Id).Add("name", thread.Name) .Add("group", thread.Group) .Add("typing", string.Equals(_source.TypingIn, thread.Id, StringComparison.OrdinalIgnoreCase)) .Add("messages", json) .Close(); } internal static string Send(string argument) { if (!_source.Online) { return "error"; } string text = argument ?? ""; int num = text.IndexOf('\n'); if (num <= 0) { return "error"; } string id = text.Substring(0, num); string text2 = text.Substring(num + 1); if (text2.IndexOf('\n') >= 0) { return "error"; } if (string.IsNullOrWhiteSpace(text2)) { return "error"; } if (text2.Trim().Length > 500) { return "error"; } Thread thread = _source.Find(id); if (thread == null) { return "error"; } if (!_source.Send(thread.Id, text2)) { return "error"; } return "ok"; } } internal sealed class Message { internal string From = ""; internal string Text = ""; internal DateTime At; internal bool Mine; internal bool Pending; } internal sealed class Thread { internal string Id = ""; internal string Name = ""; internal bool Group; internal int Unread; internal readonly List<Message> Messages = new List<Message>(); } internal static class ChatModel { internal const string GroupId = "everyone"; private static readonly string[] _replies = new string[8] { "on my way", "give me two minutes, dealing with a customer", "did you restock the west van?", "cops were on Marina Ave, take the back road", "that batch came out at 82%, best yet", "no way. seriously?", "sending the cash over now", "can you cover the motel drop tonight?" }; private static readonly List<Thread> _threads = new List<Thread>(); private static int _replyIndex; internal static Thread LastArrival; internal static string Self { get; private set; } = "You"; internal static IReadOnlyList<Thread> Threads => _threads; internal static int Revision { get; private set; } internal static bool Online { get; private set; } = true; internal static int Peers { get { int num = 0; foreach (Thread thread in _threads) { if (!thread.Group) { num++; } } return num; } } internal static string TypingIn { get; private set; } internal static void SetOnline(bool online) { if (Online != online) { Online = online; Revision++; } } internal static Thread Find(string id) { foreach (Thread thread in _threads) { if (string.Equals(thread.Id, id, StringComparison.OrdinalIgnoreCase)) { return thread; } } return null; } internal static void Seed() { _threads.Clear(); TypingIn = null; _replyIndex = 0; Online = true; Thread thread = new Thread { Id = "everyone", Name = "Everyone", Group = true }; Add(thread, "coolpaca", "anyone seen my van keys", -46); Add(thread, "wesconsin", "they were in the ignition. again.", -44); Add(thread, "SirTidez", "meeting at the docks at nine, bring the van", -12); thread.Unread = 1; _threads.Add(thread); Thread thread2 = new Thread { Id = "tidez", Name = "SirTidez" }; Add(thread2, "SirTidez", "you at the warehouse?", -180); Add(thread2, Self, "on my way, five minutes", -176, mine: true); Add(thread2, "SirTidez", "nice, door is open", -175); _threads.Add(thread2); Thread thread3 = new Thread { Id = "bars", Name = "Bars", Unread = 2 }; Add(thread3, "Bars", "we are two short for tonight", -9); Add(thread3, "Bars", "can you bring someone?", -8); _threads.Add(thread3); Thread thread4 = new Thread { Id = "eternidox", Name = "Eternidox" }; Add(thread4, Self, "storage is full, we need a second place", -320, mine: true); Add(thread4, "Eternidox", "i know a guy who knows a guy", -318); _threads.Add(thread4); Revision++; } internal static void Send(string threadId, string text) { Thread thread = Find(threadId); if (thread != null && !string.IsNullOrWhiteSpace(text)) { thread.Messages.Add(new Message { From = Self, Text = text.Trim(), At = DateTime.Now, Mine = true, Pending = true }); Revision++; } } internal static bool ConfirmPending() { bool flag = false; foreach (Thread thread in _threads) { foreach (Message message in thread.Messages) { if (message.Pending) { message.Pending = false; flag = true; } } } if (flag) { Revision++; } return flag; } internal static void SetTyping(string threadId) { string text = Find(threadId)?.Id; if (!(TypingIn == text)) { TypingIn = text; Revision++; } } internal static void Reply(string threadId) { Thread thread = Find(threadId); if (thread != null) { TypingIn = null; string text = (thread.Group ? "SirTidez" : thread.Name); thread.Messages.Add(new Message { From = text, Text = _replies[_replyIndex++ % _replies.Length], At = DateTime.Now }); thread.Unread++; Revision++; LastArrival = thread; } } internal static void MarkRead(string threadId) { Thread thread = Find(threadId); if (thread != null && thread.Unread != 0) { thread.Unread = 0; Revision++; } } internal static int TotalUnread() { int num = 0; foreach (Thread thread in _threads) { num += thread.Unread; } return num; } internal static string Clock(DateTime at) { return at.ToString("HH:mm", CultureInfo.InvariantCulture); } internal static string DayLabel(DateTime at) { return DayLabel(at, DateTime.Now); } internal static string DayLabel(DateTime at, DateTime now) { DateTime date = now.Date; if (at.Date == date) { return "Today"; } if (at.Date == date.AddDays(-1.0)) { return "Yesterday"; } return at.ToString("d MMMM", CultureInfo.InvariantCulture); } private static void Add(Thread thread, string from, string text, int minutesAgo, bool mine = false) { thread.Messages.Add(new Message { From = from, Text = text, At = DateTime.Now.AddMinutes(minutesAgo), Mine = mine }); } } internal sealed class DemoSource : IChatSource { private DateTime _confirmAt = DateTime.MaxValue; private DateTime _typingAt = DateTime.MaxValue; private DateTime _replyAt = DateTime.MaxValue; private string _replyTo; public string Self => ChatModel.Self; public int Revision => ChatModel.Revision; public bool Online => ChatModel.Online; public int Peers => ChatModel.Peers; public IReadOnlyList<Thread> Threads => ChatModel.Threads; public string TypingIn => ChatModel.TypingIn; internal DemoSource() { ChatModel.Seed(); _confirmAt = (_typingAt = (_replyAt = DateTime.MaxValue)); _replyTo = null; } public Thread Find(string id) { return ChatModel.Find(id); } public void MarkRead(string threadId) { ChatModel.MarkRead(threadId); } public int TotalUnread() { return ChatModel.TotalUnread(); } public Thread TakeArrival() { Thread lastArrival = ChatModel.LastArrival; ChatModel.LastArrival = null; return lastArrival; } public bool Send(string threadId, string text) { Thread thread = ChatModel.Find(threadId); if (thread == null) { return false; } ChatModel.Send(thread.Id, text); _confirmAt = DateTime.Now.AddSeconds(0.9); _typingAt = DateTime.Now.AddSeconds(1.8); _replyAt = DateTime.Now.AddSeconds(4.0); _replyTo = thread.Id; return true; } public void Tick() { DateTime now = DateTime.Now; if (now >= _confirmAt) { _confirmAt = DateTime.MaxValue; ChatModel.ConfirmPending(); } if (now >= _typingAt) { _typingAt = DateTime.MaxValue; ChatModel.SetTyping(_replyTo); } if (now >= _replyAt) { _replyAt = DateTime.MaxValue; ChatModel.Reply(_replyTo); } } } internal interface IChatSource { string Self { get; } int Revision { get; } bool Online { get; } int Peers { get; } IReadOnlyList<Thread> Threads { get; } string TypingIn { get; } Thread Find(string id); bool Send(string threadId, string text); void MarkRead(string threadId); int TotalUnread(); Thread TakeArrival(); void Tick(); } internal sealed class Json { private readonly StringBuilder _sb = new StringBuilder(); private bool _empty = true; private string _closed; private Json(char open) { _sb.Append(open); } internal static Json Object() { return new Json('{'); } internal static Json Array() { return new Json('['); } internal Json Add(string key, string value) { Separate(); Quote(key); _sb.Append(':'); Quote(value); return this; } internal Json Add(string key, long value) { Separate(); Quote(key); _sb.Append(':').Append(value); return this; } internal Json Add(string key, bool value) { Separate(); Quote(key); _sb.Append(':').Append(value ? "true" : "false"); return this; } internal Json Add(string key, Json value) { Separate(); Quote(key); _sb.Append(':').Append(value.Close()); return this; } internal Json Item(Json value) { Separate(); _sb.Append(value.Close()); return this; } internal string Close() { if (_closed != null) { return _closed; } _sb.Append((_sb[0] == '{') ? '}' : ']'); return _closed = _sb.ToString(); } public override string ToString() { return Close(); } private void Separate() { if (_closed != null) { throw new InvalidOperationException("this JSON value is already closed and cannot be added to"); } if (!_empty) { _sb.Append(','); } _empty = false; } private void Quote(string value) { _sb.Append('"'); string text = value ?? ""; foreach (char c in text) { switch (c) { case '"': _sb.Append("\\\""); continue; case '\\': _sb.Append("\\\\"); continue; case '\n': _sb.Append("\\n"); continue; case '\r': _sb.Append("\\r"); continue; case '\t': _sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = _sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4")); } else { _sb.Append(c); } } _sb.Append('"'); } } }