using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using CatLib.Config;
using CatLib.Core;
using CatLib.DevTools;
using CatLib.Game.Events;
using CatLib.Localization;
using CatLib.Logging;
using CatLib.Net;
using CatLib.Patching;
using CatLib.Threading;
using CatLib.UI;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppMicrosoft.Data.Sqlite;
using Il2CppSystem;
using Il2CppSystem.Collections.Generic;
using Il2CppSystem.Data.Common;
using Il2CppSystem.Reflection;
using Microsoft.CodeAnalysis;
using TMPro;
using TooLate.Logic;
using TooLate.Patches;
using TooLate.Research;
using TooLate.Scene;
using TooLate.Settings;
using UnityEngine;
using UnityEngine.UI;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("CatLib contributors")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+1b73f398a8bbe07fec967f69ef1e969788e687bd")]
[assembly: AssemblyProduct("CatLib")]
[assembly: AssemblyTitle("TooLate")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
internal sealed class IsUnmanagedAttribute : Attribute
{
}
[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 TooLate
{
[BepInPlugin("catlib.toolate", "Too Late", "0.1.1")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public sealed class TooLatePlugin : BasePlugin
{
public const string LanguageResourcePrefix = "TooLate.Lang.";
public const string DevGroup = "Too Late";
public static JoinCoordinator Joins { get; private set; }
public override void Load()
{
CatLogger val = CatLogger.From(((BasePlugin)this).Log);
CatSettings val2 = CatSettings.For((BasePlugin)(object)this);
int count = val2.Texts.Count;
CatNetwork.Declare((BasePlugin)(object)this, (SessionPolicy)2, (VersionRule)2);
JoinCoordinator joins = new JoinCoordinator(val, val2.Texts);
joins.Settings = new JoinSettings(val2, null);
Joins = joins;
MethodInfo methodInfo = typeof(Server).GetMethods().FirstOrDefault((MethodInfo method) => method.Name == "HandleClientConnect" && method.GetParameters().Length == 2 && method.GetParameters()[0].ParameterType == typeof(ulong));
CodePatch loading = CodePatch.Locate("catlib.toolate", "joining a level", val, (MethodBase)methodInfo, "44 38 A1 90 00 00 00 0F 85 ?? ?? ?? ??", 7, 6, 2048);
CodePatch limit = CodePatch.Locate("catlib.toolate", "player limit", val, (MethodBase)methodInfo, "E8 ?? ?? ?? ?? 83 F8 05 0F 8D", 7, 1, 2048);
joins.UseCodePatches(loading, limit);
LobbyOverflow lobby = new LobbyOverflow(val, val2.Texts);
MessageRecorder recorder = new MessageRecorder(val);
CatPatches patches = TooLatePatches.Install("catlib.toolate", joins, lobby, recorder, val);
patches.TurnedOff += delegate
{
MainThread.Post((Action)joins.ShutDown);
};
FrameLoop.Update += delegate
{
patches.Run("Update", (Action)joins.Update);
patches.Run("Recorder", (Action)recorder.Update);
};
NetworkEvents.ClientConnected += delegate(ulong id)
{
patches.Run("ClientConnected", (Action)delegate
{
joins.OnClientConnected(id);
});
};
NetworkEvents.ClientDisconnected += delegate(ulong id)
{
patches.Run("ClientDisconnected", (Action)delegate
{
joins.OnClientDisconnected(id);
});
};
BootstrapEvents.GameStartedFromLobby += delegate
{
patches.Run("GameStarted", (Action)joins.EnterLevel);
};
BootstrapEvents.LevelLoadStarted += delegate
{
patches.Run("LevelLoadStarted", (Action)joins.EnterLevel);
};
BootstrapEvents.GameRestartStarted += delegate
{
joins.LeaveLevel(disconnectWaiting: true);
};
BootstrapEvents.MainMenuLoaded += delegate
{
joins.LeaveLevel(disconnectWaiting: false);
lobby.Forget();
};
DevMenu.Toggle("Too Late", "Record game messages", (Func<bool>)(() => recorder.IsEnabled), (Action<bool>)recorder.SetEnabled, "Writes every network message of the game to the log: what the host sends to each player and what this game sends to the host.");
DevMenu.Command("Too Late", "Joining players in the log", (Func<string>)joins.Describe, "Writes whether players can join your level now, the player limit and every player who is joining.");
DevMenu.Command("Too Late", "Snapshot in the log", (Func<string>)joins.SnapshotProbe, "Writes the warehouse as it is now into a separate file in BepInEx/cache/TooLate, like for a joining player, and writes what it holds.");
val.Info($"Too Late {"0.1.1"} loaded with {count} translated text(s) in {string.Join(", ", val2.Texts.Languages)}, " + ((patches.IsActive && joins.CanJoinInLevel) ? "players can join your levels" : "joining a level stays closed like in the game") + (joins.CanRaiseLimit ? string.Empty : ", the player limit stays 4"));
}
}
public static class PluginMeta
{
public const string Guid = "catlib.toolate";
public const string Name = "Too Late";
public const string Version = "0.1.1";
}
}
namespace TooLate.Settings
{
public sealed class JoinSettings
{
public Setting<bool> Enabled { get; }
public Setting<int> MaxPlayers { get; }
public Setting<bool> Messages { get; }
public JoinSettings(CatSettings settings, Action changed)
{
Enabled = settings.Local<bool>("Join", "Enabled", true, "Players can join while you are already playing. They get the warehouse as it is right now. Only the host needs the mod.", (AcceptableValueBase)null);
MaxPlayers = settings.Local<int>("Join", "MaxPlayers", 4, "How many players fit in your game, you included. 4 is the game's limit. Above 4 the lobby shows the extra players as a count, and the counters stay four.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(4, 8));
Messages = settings.Local<bool>("Messages", "Enabled", true, "Tells you when a player starts joining, waits for the end of the day's results and is in the game. Only for you.", (AcceptableValueBase)null);
Enabled.Changed += delegate
{
changed?.Invoke();
};
MaxPlayers.Changed += delegate
{
changed?.Invoke();
};
}
}
}
namespace TooLate.Scene
{
public static class GameProtocol
{
[ThreadStatic]
private static bool _sending;
public static bool IsSending => _sending;
public static NetworkManager Network
{
get
{
if (!Singleton<NetworkManager>.HasInstance())
{
return null;
}
return Singleton<NetworkManager>.Instance;
}
}
public static Server Server
{
get
{
NetworkManager network = Network;
if (!((Object)(object)network == (Object)null) && network.IsServer)
{
return network._server;
}
return null;
}
}
public static bool IsHosting => Server != null;
public static Il2CppReferenceArray<Object> Empty()
{
return new Il2CppReferenceArray<Object>(0L);
}
public static bool Send(int code, ulong target, Il2CppReferenceArray<Object> payload, bool reliable = true)
{
Server server = Server;
if (server == null)
{
return false;
}
bool sending = _sending;
_sending = true;
try
{
server.SendMessage((ProtocolCode)code, reliable, target, payload ?? Empty());
return true;
}
finally
{
_sending = sending;
}
}
public static void SendSave(ulong target, string saveName, byte[] data)
{
NetworkManager val = Network ?? throw new InvalidOperationException("The game has no network manager");
bool sending = _sending;
_sending = true;
try
{
val.SendSaveSynchronization(target, false, false, false, saveName, Il2CppStructArray<byte>.op_Implicit(data));
}
finally
{
_sending = sending;
}
}
public static void Disconnect(ulong target)
{
Server server = Server;
if (server != null)
{
server.DisconnectClient(target);
}
}
public static Il2CppReferenceArray<Object> Copy(Il2CppReferenceArray<Object> payload)
{
if (payload == null)
{
return Empty();
}
Il2CppReferenceArray<Object> val = new Il2CppReferenceArray<Object>((long)((Il2CppArrayBase<Object>)(object)payload).Length);
for (int i = 0; i < ((Il2CppArrayBase<Object>)(object)payload).Length; i++)
{
((Il2CppArrayBase<Object>)(object)val)[i] = ((Il2CppArrayBase<Object>)(object)payload)[i];
}
return val;
}
public static Il2CppReferenceArray<Object> Identifiers(IReadOnlyList<EntityPlace> places, uint nextIdentifier)
{
//IL_0055: Unknown result type (might be due to invalid IL or missing references)
Il2CppReferenceArray<Object> val = new Il2CppReferenceArray<Object>((long)(places.Count * 2 + 2));
int num = 0;
foreach (EntityPlace place in places)
{
((Il2CppArrayBase<Object>)(object)val)[num++] = Box(place.Id);
((Il2CppArrayBase<Object>)(object)val)[num++] = Box(new Vector3(place.X, place.Y, place.Z));
}
((Il2CppArrayBase<Object>)(object)val)[num++] = Box(uint.MaxValue);
((Il2CppArrayBase<Object>)(object)val)[num] = Box(nextIdentifier);
return val;
}
public static Il2CppReferenceArray<Object> Generic(uint entityId, string text)
{
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_002f: Expected O, but got Unknown
return new Il2CppReferenceArray<Object>(2L)
{
[0] = Box(entityId),
[1] = new Object(IL2CPP.ManagedStringToIl2Cpp(text ?? string.Empty))
};
}
public static bool TryReadGeneric(Il2CppReferenceArray<Object> payload, out uint entityId, out string text)
{
text = null;
if (TryReadId(payload, out entityId) && ((Il2CppArrayBase<Object>)(object)payload).Length >= 2 && ((Il2CppArrayBase<Object>)(object)payload)[1] != null)
{
Type il2CppType = ((Il2CppArrayBase<Object>)(object)payload)[1].GetIl2CppType();
if (!(((il2CppType != null) ? ((MemberInfo)il2CppType).Name : null) != "String"))
{
text = IL2CPP.Il2CppStringToManaged(((Il2CppObjectBase)((Il2CppArrayBase<Object>)(object)payload)[1]).Pointer);
return text != null;
}
}
return false;
}
public static bool TryReadId(Il2CppReferenceArray<Object> payload, out uint entityId)
{
entityId = 0u;
if (payload != null && ((Il2CppArrayBase<Object>)(object)payload).Length >= 1 && ((Il2CppArrayBase<Object>)(object)payload)[0] != null)
{
Type il2CppType = ((Il2CppArrayBase<Object>)(object)payload)[0].GetIl2CppType();
if (!(((il2CppType != null) ? ((MemberInfo)il2CppType).Name : null) != "UInt32"))
{
entityId = ((Il2CppObjectBase)((Il2CppArrayBase<Object>)(object)payload)[0]).Unbox<uint>();
return true;
}
}
return false;
}
public static Il2CppReferenceArray<Object> TimePeriod(int period)
{
return new Il2CppReferenceArray<Object>(1L) { [0] = Box((byte)period, ClassOf<byte, Byte>()) };
}
public static Object Box(uint value)
{
return Box(value, ClassOf<uint, UInt32>());
}
public static Object Box(Vector3 value)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
return Box<Vector3>(value, ClassOf<Vector3, Vector3>());
}
private unsafe static Object Box<T>(T value, IntPtr type) where T : unmanaged
{
//IL_003a: Unknown result type (might be due to invalid IL or missing references)
//IL_0040: Expected O, but got Unknown
if (type == IntPtr.Zero)
{
throw new InvalidOperationException("The game has no class for " + typeof(T).Name);
}
return new Object(IL2CPP.il2cpp_value_box(type, (IntPtr)(&value)));
}
private static IntPtr ClassOf<TManaged, TGame>()
{
RuntimeHelpers.RunClassConstructor(typeof(TGame).TypeHandle);
IntPtr nativeClassPtr = Il2CppClassPointerStore<TManaged>.NativeClassPtr;
if (!(nativeClassPtr != IntPtr.Zero))
{
return Il2CppClassPointerStore<TGame>.NativeClassPtr;
}
return nativeClassPtr;
}
public static string Describe(Il2CppReferenceArray<Object> payload, int most = 6)
{
if (payload == null || ((Il2CppArrayBase<Object>)(object)payload).Length == 0)
{
return "no values";
}
List<string> list = new List<string>();
for (int i = 0; i < ((Il2CppArrayBase<Object>)(object)payload).Length && i < most; i++)
{
list.Add(DescribeValue(((Il2CppArrayBase<Object>)(object)payload)[i]));
}
if (((Il2CppArrayBase<Object>)(object)payload).Length > most)
{
list.Add($"... {((Il2CppArrayBase<Object>)(object)payload).Length - most} more");
}
return $"{((Il2CppArrayBase<Object>)(object)payload).Length} value(s): {string.Join(", ", list)}";
}
private static string DescribeValue(Object value)
{
//IL_0212: Unknown result type (might be due to invalid IL or missing references)
//IL_0217: Unknown result type (might be due to invalid IL or missing references)
//IL_022f: Unknown result type (might be due to invalid IL or missing references)
//IL_024d: Unknown result type (might be due to invalid IL or missing references)
//IL_026b: Unknown result type (might be due to invalid IL or missing references)
if (value == null)
{
return "null";
}
try
{
Type il2CppType = value.GetIl2CppType();
string text = ((il2CppType == (Type)null) ? "?" : ((MemberInfo)il2CppType).Name);
switch (text)
{
case "String":
{
string text2 = IL2CPP.Il2CppStringToManaged(((Il2CppObjectBase)value).Pointer) ?? string.Empty;
return (text2.Length > 40) ? ("\"" + text2.Substring(0, 40) + "...\"") : ("\"" + text2 + "\"");
}
case "Boolean":
return ((Il2CppObjectBase)value).Unbox<bool>() ? "true" : "false";
case "Int32":
return ((Il2CppObjectBase)value).Unbox<int>().ToString();
case "UInt32":
return ((Il2CppObjectBase)value).Unbox<uint>().ToString();
case "UInt64":
return ((Il2CppObjectBase)value).Unbox<ulong>().ToString();
case "Single":
return ((Il2CppObjectBase)value).Unbox<float>().ToString("0.###");
case "Vector3":
{
Vector3 val = ((Il2CppObjectBase)value).Unbox<Vector3>();
return $"({val.x:0.##}, {val.y:0.##}, {val.z:0.##})";
}
case "Byte[]":
return $"{((Il2CppArrayBase<byte>)(object)new Il2CppStructArray<byte>(((Il2CppObjectBase)value).Pointer)).Length} bytes";
default:
return text;
}
}
catch (Exception ex)
{
return "? " + ex.GetType().Name;
}
}
}
public sealed class JoinCoordinator
{
public const double LoadingWarningSeconds = 120.0;
public const double LoadingGiveUpSeconds = 360.0;
public const int FlushPerFrame = 200;
public const double AppearingGraceSeconds = 3.0;
public const string EventPrefix = "TooLate.";
private readonly CatLogger _log;
private readonly TextCatalog _texts;
private readonly Dictionary<ulong, JoinTicket<Il2CppReferenceArray<Object>>> _tickets = new Dictionary<ulong, JoinTicket<Il2CppReferenceArray<Object>>>();
private readonly HashSet<ulong> _warned = new HashSet<ulong>();
private readonly StateLedger _state = new StateLedger();
private readonly LastingMessages<Il2CppReferenceArray<Object>> _lasting = new LastingMessages<Il2CppReferenceArray<Object>>();
private CodePatch _loading;
private CodePatch _limit;
private bool _loadingOpen;
private bool _startWritten;
private bool _inLevel;
private int _limitWritten = 4;
public JoinSettings Settings { get; set; }
public IReadOnlyCollection<JoinTicket<Il2CppReferenceArray<Object>>> Tickets => _tickets.Values;
public bool CanJoinInLevel
{
get
{
CodePatch loading = _loading;
if (loading != null)
{
return loading.IsFound;
}
return false;
}
}
public bool CanRaiseLimit
{
get
{
CodePatch limit = _limit;
if (limit != null)
{
return limit.IsFound;
}
return false;
}
}
public bool InLevel => _inLevel;
public JoinCoordinator(CatLogger log, TextCatalog texts)
{
_log = log;
_texts = texts;
}
public void UseCodePatches(CodePatch loading, CodePatch limit)
{
_loading = loading;
_limit = limit;
}
public void Update()
{
HostState state = ReadHost();
UpdateCodePatches(state);
if (!state.Hosting)
{
if (_tickets.Count > 0)
{
_tickets.Clear();
}
return;
}
foreach (JoinTicket<Il2CppReferenceArray<Object>> item in _tickets.Values.ToList())
{
if (item.FailReason != null)
{
Fail(item, item.FailReason);
continue;
}
if (item.Flushing)
{
Flush(item);
continue;
}
switch (item.Stage)
{
case JoinStage.Waiting:
if (JoinGate.ReadyToStart(state, FrameLoop.Realtime - item.ConnectedAt))
{
Start(item);
}
else if (!item.WaitingNoticeShown && JoinGate.IsRecap(state.TimePeriod))
{
item.WaitingNoticeShown = true;
Notify("message.waiting", item.Name);
_log.Info(item.Name + " waits in the lobby until the day's results are over");
}
break;
case JoinStage.Appearing:
if (IsSpawned(item.ClientId) && FrameLoop.Realtime - item.StageSince > 3.0)
{
_log.Warning(item.Name + " appeared without the game sending them the identifiers again, Too Late sends them itself");
GameProtocol.Send(21, item.ClientId, GameProtocol.Identifiers(item.Places, NextIdentifier()));
StartFlush(item);
break;
}
goto case JoinStage.Loading;
case JoinStage.Loading:
if (item.Overflowed > 0)
{
Fail(item, $"more than {20000} game messages piled up while the player was loading");
}
else if (FrameLoop.Realtime - item.StageSince > 360.0)
{
Fail(item, $"the player did not finish loading in {360.0:0} seconds");
}
else if (FrameLoop.Realtime - item.StageSince > 120.0 && _warned.Add(item.ClientId))
{
_log.Warning($"{item.Name} has been loading for {120.0:0} seconds, {item.Held.Count} game message(s) wait for them");
}
break;
case JoinStage.Synced:
if (IsSpawned(item.ClientId))
{
item.MoveTo(JoinStage.Playing, FrameLoop.Realtime);
Notify("message.joined", item.Name);
Publish("Joined", item);
_log.Info(item.Name + " is in the game, " + Seconds(FrameLoop.Realtime - item.ConnectedAt) + " after connecting");
}
break;
}
}
}
public void OnClientConnected(ulong clientId)
{
if (ShouldTakeOver(clientId))
{
Ensure(clientId);
}
else if (GameProtocol.IsHosting && CatNetwork.IsRemotePlayer(clientId) && !_tickets.ContainsKey(clientId))
{
_log.Info($"Player {clientId} connected and joins the usual way: {(_inLevel ? "joining a game in progress is off in the settings" : "you are in the lobby")}");
}
}
public void OnClientDisconnected(ulong clientId)
{
_warned.Remove(clientId);
if (_tickets.Remove(clientId, out var value))
{
Publish("Left", value);
_log.Info(value.Name + " left while " + value.Stage.ToString().ToLowerInvariant());
}
}
public bool TakesOverSave(ulong clientId)
{
if (!ShouldTakeOver(clientId))
{
return false;
}
Ensure(clientId);
return true;
}
public void Observe(int code, Il2CppReferenceArray<Object> payload)
{
bool flag = !_inLevel;
if (!flag)
{
bool flag2 = ((code == 18 || code == 38) ? true : false);
flag = !flag2 && !LastingMessages<Object>.IsLasting(code);
}
if (flag || !GameProtocol.IsHosting)
{
return;
}
switch (code)
{
case 38:
{
if (GameProtocol.TryReadGeneric(payload, out var entityId2, out var text))
{
_state.Record(entityId2, text);
}
return;
}
case 18:
{
if (GameProtocol.TryReadId(payload, out var entityId))
{
_state.Forget(entityId);
_lasting.Forget(entityId);
}
return;
}
}
uint entityId3;
if (!LastingMessages<Object>.IsPerEntity(code))
{
_lasting.Record(code, 0u, GameProtocol.Copy(payload));
}
else if (GameProtocol.TryReadId(payload, out entityId3))
{
_lasting.Record(code, entityId3, GameProtocol.Copy(payload));
}
}
public bool RouteOutgoing(int code, bool reliable, ulong target, Il2CppReferenceArray<Object> payload)
{
if (_tickets.Count == 0 || !_tickets.TryGetValue(target, out var value))
{
return true;
}
switch (MessageRoute.For(value.Stage, code))
{
case Route.Pass:
return true;
case Route.Replace:
try
{
SendIdentifiers(value);
}
catch (Exception ex)
{
_log.Error("Sending the identifiers to " + value.Name + " failed", ex);
value.FailReason = "the identifiers of the snapshot could not be sent";
}
return false;
case Route.Hold:
case Route.HoldLatest:
value.Accept(code, reliable, GameProtocol.Copy(payload));
return false;
default:
value.Accept(code, reliable, null);
return false;
}
}
public void ShutDown()
{
_log.Warning("Too Late turned itself off: players who are still joining are disconnected, joining a level and the player limit are like in the game again");
foreach (JoinTicket<Il2CppReferenceArray<Object>> item in _tickets.Values.Where(delegate(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
JoinStage stage = ticket.Stage;
return (uint)stage <= 2u;
}).ToList())
{
GameProtocol.Disconnect(item.ClientId);
}
_tickets.Clear();
_warned.Clear();
CodePatch loading = _loading;
if (loading != null && loading.IsFound)
{
_loading.Restore();
}
loading = _limit;
if (loading != null && loading.IsFound)
{
_limit.Restore();
}
_loadingOpen = false;
_limitWritten = 4;
}
public void EnterLevel()
{
if (!_inLevel)
{
_inLevel = true;
_startWritten = false;
ClearState();
WriteStarted();
}
}
private void WriteStarted()
{
if (!_startWritten && _inLevel && GameProtocol.IsHosting)
{
_startWritten = true;
_log.Info(CatNetwork.IsWithoutSteamNetwork ? "Your game started without the Steam network, it runs on this computer only and nobody can join it" : "Your game started, players who connect from now on join the game in progress");
}
}
public void LeaveLevel(bool disconnectWaiting)
{
_inLevel = false;
ClearState();
Forget(disconnectWaiting);
}
private void Forget(bool disconnectWaiting)
{
if (disconnectWaiting)
{
foreach (JoinTicket<Il2CppReferenceArray<Object>> item in _tickets.Values.Where(delegate(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
JoinStage stage = ticket.Stage;
return (uint)stage <= 2u;
}).ToList())
{
_log.Info(item.Name + " is disconnected, the game restarts before they could join");
GameProtocol.Disconnect(item.ClientId);
}
}
_tickets.Clear();
_warned.Clear();
UpdateCodePatches(default(HostState));
}
public string Describe()
{
HostState hostState = ReadHost();
List<string> list = new List<string>();
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(118, 9);
defaultInterpolatedStringHandler.AppendLiteral("Hosting ");
defaultInterpolatedStringHandler.AppendFormatted(hostState.Hosting);
defaultInterpolatedStringHandler.AppendLiteral(", in a level ");
defaultInterpolatedStringHandler.AppendFormatted(_inLevel);
defaultInterpolatedStringHandler.AppendLiteral(", state messages ");
defaultInterpolatedStringHandler.AppendFormatted(_state.Count);
defaultInterpolatedStringHandler.AppendLiteral(" + ");
defaultInterpolatedStringHandler.AppendFormatted(_lasting.Count);
defaultInterpolatedStringHandler.AppendLiteral(", level ready ");
defaultInterpolatedStringHandler.AppendFormatted(hostState.LevelReady);
defaultInterpolatedStringHandler.AppendLiteral(", restarting ");
defaultInterpolatedStringHandler.AppendFormatted(hostState.Restarting);
defaultInterpolatedStringHandler.AppendLiteral(", time period ");
defaultInterpolatedStringHandler.AppendFormatted<GameTimePeriod>((GameTimePeriod)hostState.TimePeriod);
defaultInterpolatedStringHandler.AppendLiteral(", ");
defaultInterpolatedStringHandler.AppendLiteral("joining in a level ");
object value;
if (_loadingOpen)
{
CodePatch loading = _loading;
if (loading != null && loading.IsFound)
{
value = "open";
goto IL_0114;
}
}
value = "closed";
goto IL_0114;
IL_0114:
defaultInterpolatedStringHandler.AppendFormatted((string?)value);
defaultInterpolatedStringHandler.AppendLiteral(", player limit ");
defaultInterpolatedStringHandler.AppendFormatted(_limitWritten);
list.Add(defaultInterpolatedStringHandler.ToStringAndClear());
List<string> list2 = list;
list2.AddRange(_tickets.Values.Select((JoinTicket<Il2CppReferenceArray<Object>> ticket) => $"{ticket.Name} ({ticket.ClientId}): {ticket.Stage} for {Seconds(FrameLoop.Realtime - ticket.StageSince)}, held {ticket.Held.Count}, dropped {ticket.Dropped}, places {ticket.Places?.Count ?? 0}"));
if (_tickets.Count == 0)
{
list2.Add("Nobody is joining");
}
foreach (string item in list2)
{
_log.Info("[Joins] " + item);
}
return list2[0];
}
public string SnapshotProbe()
{
SnapshotResult snapshotResult = SaveSnapshot.Write();
string text = "Snapshot written to " + SaveSnapshot.Folder + ": " + snapshotResult;
_log.Info(text);
return text;
}
private bool ShouldTakeOver(ulong clientId)
{
if (_inLevel && Settings != null && Settings.Enabled.Value && GameProtocol.IsHosting)
{
return CatNetwork.IsRemotePlayer(clientId);
}
return false;
}
private void Ensure(ulong clientId)
{
if (!_tickets.ContainsKey(clientId))
{
JoinTicket<Il2CppReferenceArray<Object>> joinTicket = new JoinTicket<Il2CppReferenceArray<Object>>(clientId, CatNetwork.PlayerName(clientId), FrameLoop.Realtime);
_tickets[clientId] = joinTicket;
Notify("message.joining", joinTicket.Name);
Publish("Connected", joinTicket);
_log.Info($"{joinTicket.Name} ({clientId}) connected while you are in a level, they wait in the lobby until the game lets them in");
}
}
private void Start(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
try
{
SnapshotResult snapshotResult = SaveSnapshot.Write();
ticket.Places = snapshotResult.Places;
ticket.MoveTo(JoinStage.Loading, FrameLoop.Realtime);
GameProtocol.SendSave(ticket.ClientId, snapshotResult.SaveName, snapshotResult.Data);
GameProtocol.Send(10, ticket.ClientId, GameProtocol.Empty());
Publish("Loading", ticket, snapshotResult.ToString());
_log.Info($"{ticket.Name} gets the warehouse as it is now and starts loading. Snapshot {snapshotResult}");
}
catch (Exception ex)
{
_log.Error("The snapshot for " + ticket.Name + " could not be made", ex);
Fail(ticket, "the snapshot of the game could not be made");
}
}
private void SendIdentifiers(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
if (ticket.Places == null)
{
ticket.Accept(21, reliable: true, null);
return;
}
if (!ticket.PlacesMerged)
{
HashSet<uint> saved = new HashSet<uint>(ticket.Places.Select((EntityPlace place) => place.Id));
ticket.Places = EntityPlaces.Merge(SceneEntities(), ticket.Places);
ticket.ScenePlaces = ticket.Places.Where((EntityPlace place) => !saved.Contains(place.Id)).ToList();
ticket.PlacesMerged = true;
}
IReadOnlyList<EntityPlace> readOnlyList = ((ticket.Stage == JoinStage.Loading) ? ticket.ScenePlaces : ticket.Places);
GameProtocol.Send(21, ticket.ClientId, GameProtocol.Identifiers(readOnlyList, NextIdentifier()));
if (ticket.Stage == JoinStage.Appearing && !ticket.Flushing)
{
_log.Info($"{ticket.Name} appeared and has the identifiers of the saved entities now ({ticket.Places.Count}), the state of the warehouse and {ticket.Held.Count} held message(s) follow");
StartFlush(ticket);
return;
}
if (ticket.Stage != JoinStage.Loading)
{
_log.Info($"Sent {ticket.Name} the identifiers of the snapshot again ({ticket.Places.Count})");
return;
}
string text = string.Join(", ", from message in ticket.Held
group message by message.Code into @group
orderby @group.Key
select $"{MessageRecorder.Name(@group.Key)} x{@group.Count()}");
GameProtocol.Send(11, ticket.ClientId, GameProtocol.Empty());
ticket.MoveTo(JoinStage.Appearing, FrameLoop.Realtime);
_log.Info($"{ticket.Name} loaded the level: sent the {readOnlyList.Count} identifiers of the level's own entities, like the game does at this point, and the go to finish loading; {ticket.Held.Count} held message(s) wait until they appear ({((text.Length == 0) ? "none" : text)}), {ticket.Dropped} were dropped");
}
private void StartFlush(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
SendState(ticket);
ticket.Flushing = true;
}
private void SendState(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
//IL_002a: Unknown result type (might be due to invalid IL or missing references)
//IL_02a7: Unknown result type (might be due to invalid IL or missing references)
if (ticket.StateSent)
{
return;
}
ticket.StateSent = true;
GameTimeManager val = (Singleton<GameTimeManager>.HasInstance() ? Singleton<GameTimeManager>.Instance : null);
int num = (((Object)(object)val == (Object)null) ? (-1) : ((int)val.CurrentTimePeriod));
if (num >= 0)
{
GameProtocol.Send(45, ticket.ClientId, GameProtocol.TimePeriod(num));
}
HashSet<uint> hashSet = new HashSet<uint>((ticket.Places ?? Array.Empty<EntityPlace>()).Select((EntityPlace place) => place.Id));
IReadOnlyList<LastingMessage<Il2CppReferenceArray<Object>>> readOnlyList = _lasting.For(hashSet.Contains);
foreach (LastingMessage<Il2CppReferenceArray<Object>> item in readOnlyList)
{
GameProtocol.Send(item.Code, ticket.ClientId, item.Payload);
}
IReadOnlyList<StateMessage> readOnlyList2 = _state.For(hashSet.Contains);
foreach (StateMessage item2 in readOnlyList2)
{
GameProtocol.Send(38, ticket.ClientId, GameProtocol.Generic(item2.EntityId, item2.Text));
}
string text = string.Join(", ", from message in readOnlyList2
group message by message.Kind into @group
orderby @group.Count() descending, @group.Key
select $"{@group.Key} x{@group.Count()}");
string text2 = string.Join(", ", from message in readOnlyList
group message by message.Code into @group
orderby @group.Key
select $"{MessageRecorder.Name(@group.Key)} x{@group.Count()}");
_log.Info($"Sent {ticket.Name} the time of day ({((num >= 0) ? ((object)(GameTimePeriod)num/*cast due to .constrained prefix*/).ToString() : "unknown")}) and the state of the warehouse the game sent before they joined: {readOnlyList.Count} of {_lasting.Count} recorded game message(s){((text2.Length == 0) ? string.Empty : (" (" + text2 + ")"))}, {readOnlyList2.Count} of {_state.Count} entity message(s)" + ((text.Length == 0) ? string.Empty : (" (" + text + ")")));
}
private void Flush(JoinTicket<Il2CppReferenceArray<Object>> ticket)
{
foreach (HeldMessage<Il2CppReferenceArray<Object>> item in ticket.TakeHeld(200))
{
GameProtocol.Send(item.Code, ticket.ClientId, item.Payload, item.Reliable);
}
if (ticket.Held.Count <= 0)
{
ticket.Flushing = false;
ticket.MoveTo(JoinStage.Synced, FrameLoop.Realtime);
Publish("Synced", ticket, $"places={ticket.Places.Count} dropped={ticket.Dropped}");
_log.Info(ticket.Name + " got every held message, the game goes on for them as for everyone");
}
}
private void ClearState()
{
_state.Clear();
_lasting.Clear();
}
private static List<EntityPlace> SceneEntities()
{
//IL_0071: Unknown result type (might be due to invalid IL or missing references)
//IL_0076: Unknown result type (might be due to invalid IL or missing references)
//IL_007f: Unknown result type (might be due to invalid IL or missing references)
//IL_0086: Unknown result type (might be due to invalid IL or missing references)
//IL_008d: Unknown result type (might be due to invalid IL or missing references)
List<EntityPlace> list = new List<EntityPlace>();
NetworkedEntityManager obj = (Singleton<NetworkedEntityManager>.HasInstance() ? Singleton<NetworkedEntityManager>.Instance : null);
List<EntityNetwork> val = ((obj != null) ? obj._initialNetworkedEntities : null);
if (val == null)
{
return list;
}
for (int i = 0; i < val.Count; i++)
{
EntityNetwork val2 = val[i];
if (!((Object)(object)val2 == (Object)null) && !((Il2CppObjectBase)val2).WasCollected && Object.op_Implicit((Object)(object)val2))
{
Entity linkedEntity = val2.LinkedEntity;
if (!((Object)(object)linkedEntity != (Object)null) || ((Il2CppObjectBase)linkedEntity).WasCollected || !linkedEntity.IsDisposed)
{
Vector3 initialPosition = val2.InitialPosition;
list.Add(new EntityPlace(val2.NetworkIdentifier, initialPosition.x, initialPosition.y, initialPosition.z));
}
}
}
return list;
}
private static uint NextIdentifier()
{
if (!Singleton<NetworkedEntityManager>.HasInstance())
{
return 0u;
}
return Singleton<NetworkedEntityManager>.Instance._nextIdentifier;
}
private void Fail(JoinTicket<Il2CppReferenceArray<Object>> ticket, string reason)
{
_tickets.Remove(ticket.ClientId);
Notify("message.failed", ticket.Name);
Publish("Failed", ticket, reason);
_log.Error(ticket.Name + " cannot join: " + reason + "; they are disconnected");
GameProtocol.Disconnect(ticket.ClientId);
}
private void UpdateCodePatches(HostState state)
{
WriteStarted();
int num;
int num2;
if (Settings != null)
{
num = (Settings.Enabled.Value ? 1 : 0);
if (num != 0)
{
num2 = (JoinGate.CanAccept(state) ? 1 : 0);
goto IL_002d;
}
}
else
{
num = 0;
}
num2 = 0;
goto IL_002d;
IL_002d:
bool flag = (byte)num2 != 0;
CodePatch loading = _loading;
if (loading != null && loading.IsFound && flag != _loadingOpen && (flag ? _loading.Write(PlayerLimit.SkipJump) : _loading.Restore()))
{
_loadingOpen = flag;
_log.Info((!flag) ? "Joining your level is closed, like in the game" : (CatNetwork.IsWithoutSteamNetwork ? "Joining your level is open, but without the Steam network nobody can reach it" : "Players can join your level now"));
}
int num3 = ((num != 0 && Settings != null) ? PlayerLimit.Clamp(Settings.MaxPlayers.Value) : 4);
loading = _limit;
if (loading != null && loading.IsFound && num3 != _limitWritten && ((num3 == 4) ? _limit.Restore() : _limit.Write(new byte[1] { PlayerLimit.ConnectionLimit(num3) })))
{
_limitWritten = num3;
_log.Info($"Up to {num3} players can be in your game");
}
}
private static HostState ReadHost()
{
//IL_0053: Unknown result type (might be due to invalid IL or missing references)
BootstrapManager val = (Singleton<BootstrapManager>.HasInstance() ? Singleton<BootstrapManager>.Instance : null);
GameTimeManager val2 = (Singleton<GameTimeManager>.HasInstance() ? Singleton<GameTimeManager>.Instance : null);
return new HostState(GameProtocol.IsHosting, (Object)(object)val != (Object)null && val.LoadFinalized, (Object)(object)val != (Object)null && val.IsRestarting, ((Object)(object)val2 == (Object)null) ? (-1) : ((int)val2.CurrentTimePeriod));
}
private static bool IsSpawned(ulong clientId)
{
PlayerManager val = (Singleton<PlayerManager>.HasInstance() ? Singleton<PlayerManager>.Instance : null);
PlayerEntityBase val2 = default(PlayerEntityBase);
if ((Object)(object)val != (Object)null && val.TryGetPlayer(clientId, ref val2))
{
return (Object)(object)val2 != (Object)null;
}
return false;
}
private void Notify(string key, string name)
{
if (Settings == null || Settings.Messages.Value)
{
Notifications.Show(_texts.Format(key, new object[1] { name }), (string)null);
}
}
private static void Publish(string step, JoinTicket<Il2CppReferenceArray<Object>> ticket, string details = null)
{
GameEventStream.Publish("TooLate." + step, $"client={ticket.ClientId}" + (string.IsNullOrEmpty(details) ? string.Empty : (" " + details)));
}
private static string Seconds(double seconds)
{
return seconds.ToString("0.0", CultureInfo.InvariantCulture) + " s";
}
}
public sealed class LobbyOverflow
{
public const string LabelName = "text_TooLateMorePlayers";
public const float Gap = 24f;
public const float LabelWidth = 260f;
public const float LabelHeight = 60f;
private readonly CatLogger _log;
private readonly TextCatalog _texts;
private readonly List<ulong> _hidden = new List<ulong>();
private IntPtr _lobby;
private TMP_Text _label;
public IReadOnlyList<ulong> Hidden => _hidden;
public LobbyOverflow(CatLogger log, TextCatalog texts)
{
_log = log;
_texts = texts;
}
public void OnJoined(MultiplayerLobbyInterface lobby, ulong steamId)
{
Track(lobby);
if (steamId == 0L || HasSlot(lobby, steamId) || _hidden.Contains(steamId))
{
Refresh(lobby);
return;
}
_hidden.Add(steamId);
_log.Info($"[Lobby] {steamId} has no card in the lobby, {_hidden.Count} player(s) shown as a count");
Refresh(lobby);
}
public void OnLeft(MultiplayerLobbyInterface lobby, ulong steamId)
{
Track(lobby);
_hidden.Remove(steamId);
if (_hidden.Count > 0 && HasEmptySlot(lobby))
{
ulong num = _hidden[0];
_hidden.RemoveAt(0);
lobby.ProcessClientJoin(num);
}
Refresh(lobby);
}
public void Forget()
{
_hidden.Clear();
_lobby = IntPtr.Zero;
_label = null;
}
private void Track(MultiplayerLobbyInterface lobby)
{
if ((Object)(object)lobby != (Object)null && ((Il2CppObjectBase)lobby).Pointer != _lobby)
{
Forget();
_lobby = ((Il2CppObjectBase)lobby).Pointer;
}
}
private static bool HasSlot(MultiplayerLobbyInterface lobby, ulong steamId)
{
List<ConnectedPlayerHelper> playerSlots = lobby._playerSlots;
int num = 0;
while (playerSlots != null && num < playerSlots.Count)
{
ConnectedPlayerHelper val = playerSlots[num];
if ((Object)(object)val != (Object)null && !val.IsEmpty && val.SteamId == steamId)
{
return true;
}
num++;
}
return false;
}
private static bool HasEmptySlot(MultiplayerLobbyInterface lobby)
{
List<ConnectedPlayerHelper> playerSlots = lobby._playerSlots;
int num = 0;
while (playerSlots != null && num < playerSlots.Count)
{
if ((Object)(object)playerSlots[num] != (Object)null && playerSlots[num].IsEmpty)
{
return true;
}
num++;
}
return false;
}
private void Refresh(MultiplayerLobbyInterface lobby)
{
if (_hidden.Count == 0)
{
if ((Object)(object)_label != (Object)null && !((Il2CppObjectBase)_label).WasCollected)
{
((Component)_label).gameObject.SetActive(false);
}
return;
}
TMP_Text val = Label(lobby);
if (!((Object)(object)val == (Object)null))
{
val.text = _texts.Plural("lobby.more", (long)_hidden.Count, new object[1] { _hidden.Count });
((Component)val).gameObject.SetActive(true);
}
}
private TMP_Text Label(MultiplayerLobbyInterface lobby)
{
//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
//IL_00da: Unknown result type (might be due to invalid IL or missing references)
//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
//IL_010e: Unknown result type (might be due to invalid IL or missing references)
//IL_0116: Unknown result type (might be due to invalid IL or missing references)
//IL_011b: Unknown result type (might be due to invalid IL or missing references)
//IL_0125: Unknown result type (might be due to invalid IL or missing references)
//IL_0130: Unknown result type (might be due to invalid IL or missing references)
//IL_013f: Unknown result type (might be due to invalid IL or missing references)
//IL_0144: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)_label != (Object)null && !((Il2CppObjectBase)_label).WasCollected)
{
return _label;
}
HorizontalLayoutGroup connectedPlayerParent = lobby.ConnectedPlayerParent;
TMP_Text waitingForHostText = lobby.WaitingForHostText;
if ((Object)(object)connectedPlayerParent == (Object)null || (Object)(object)waitingForHostText == (Object)null)
{
_log.Warning("[Lobby] The lobby has no player row or text to copy, the count of more players is not shown");
return null;
}
RectTransform component = ((Component)connectedPlayerParent).GetComponent<RectTransform>();
GameObject val = Object.Instantiate<GameObject>(((Component)waitingForHostText).gameObject, ((Transform)component).parent, false);
((Object)val).name = "text_TooLateMorePlayers";
_label = val.GetComponent<TMP_Text>();
_label.enableAutoSizing = false;
_label.alignment = (TextAlignmentOptions)4097;
_label.textWrappingMode = (TextWrappingModes)0;
RectTransform rectTransform = _label.rectTransform;
Vector2 anchorMin = (rectTransform.anchorMax = component.anchorMin);
rectTransform.anchorMin = anchorMin;
rectTransform.pivot = new Vector2(0f, 0.5f);
rectTransform.sizeDelta = new Vector2(260f, 60f);
Il2CppStructArray<Vector3> val2 = new Il2CppStructArray<Vector3>(4L);
component.GetWorldCorners(val2);
((Transform)rectTransform).position = (((Il2CppArrayBase<Vector3>)(object)val2)[2] + ((Il2CppArrayBase<Vector3>)(object)val2)[3]) * 0.5f;
rectTransform.anchoredPosition += new Vector2(24f, 0f);
return _label;
}
}
public sealed class SnapshotResult
{
public string SaveName { get; set; }
public byte[] Data { get; set; }
public IReadOnlyList<EntityPlace> Places { get; set; }
public int Stamps { get; set; }
public bool FromScratch { get; set; }
public long Milliseconds { get; set; }
public override string ToString()
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(33, 6);
defaultInterpolatedStringHandler.AppendFormatted(SaveName);
defaultInterpolatedStringHandler.AppendLiteral(": ");
byte[] data = Data;
defaultInterpolatedStringHandler.AppendFormatted((data != null) ? data.Length : 0);
defaultInterpolatedStringHandler.AppendLiteral(" bytes, ");
defaultInterpolatedStringHandler.AppendFormatted(Places?.Count ?? 0);
defaultInterpolatedStringHandler.AppendLiteral(" entities, ");
defaultInterpolatedStringHandler.AppendFormatted(Stamps);
defaultInterpolatedStringHandler.AppendLiteral(" stamps");
defaultInterpolatedStringHandler.AppendFormatted(FromScratch ? ", built without a save file" : string.Empty);
defaultInterpolatedStringHandler.AppendLiteral(", ");
defaultInterpolatedStringHandler.AppendFormatted(Milliseconds);
defaultInterpolatedStringHandler.AppendLiteral(" ms");
return defaultInterpolatedStringHandler.ToStringAndClear();
}
}
public static class SaveSnapshot
{
public const string FolderName = "TooLate";
public const string FileName = "snapshot.bin";
public static readonly string[] CopiedTables = new string[3] { "Metadata", "Generic", "ProgressionGeneric" };
public static string Folder => Path.Combine(Paths.CachePath, "TooLate");
public static SnapshotResult Write()
{
//IL_009c: Unknown result type (might be due to invalid IL or missing references)
//IL_00a3: Expected O, but got Unknown
Stopwatch stopwatch = Stopwatch.StartNew();
SaveManager val = (Singleton<SaveManager>.HasInstance() ? Singleton<SaveManager>.Instance : null);
if ((Object)(object)val == (Object)null)
{
throw new InvalidOperationException("The game has no save manager");
}
string currentSaveFileName = val._currentSaveFileName;
if (string.IsNullOrEmpty(currentSaveFileName))
{
throw new InvalidOperationException("The game has no current save");
}
Directory.CreateDirectory(Folder);
string text = Path.Combine(Folder, "snapshot.bin");
string gameSaveFullPath = val.GetGameSaveFullPath(currentSaveFileName);
SqliteConnection.ClearAllPools();
if (File.Exists(text))
{
File.Delete(text);
}
bool flag = !File.Exists(gameSaveFullPath);
List<EntityPlace> list = new List<EntityPlace>();
int stamps = 0;
SqliteConnection val2 = new SqliteConnection("Data Source=" + text);
try
{
((DbConnection)val2).Open();
SqliteCommand command = val2.CreateCommand();
CreateTables(command);
if (flag)
{
AddMetadata(command, val);
}
else
{
CopyProgress(command, gameSaveFullPath);
}
SqliteTransaction val3 = val2.BeginTransaction();
SqliteCommand val4 = val2.CreateCommand();
val4.Transaction = val3;
if (!val.ExecuteSaveEntities(val4))
{
throw new InvalidOperationException("The game could not write its entities into the snapshot");
}
if (!val.ExecuteSaveStamps(val4))
{
throw new InvalidOperationException("The game could not write its stamps into the snapshot");
}
if (!val.ExecuteEnqueuedGenericSaveData(val4))
{
throw new InvalidOperationException("The game could not write its progress into the snapshot");
}
((DbTransaction)val3).Commit();
SqliteCommand val5 = val2.CreateCommand();
((DbCommand)val5).CommandText = "SELECT Id, PositionX, PositionY, PositionZ FROM Entity";
SqliteDataReader val6 = val5.ExecuteReader();
while (((DbDataReader)val6).Read())
{
if (!((DbDataReader)val6).IsDBNull(0))
{
list.Add(new EntityPlace((uint)Number(((DbDataReader)val6).GetValue(0)), (float)Number(((DbDataReader)val6).GetValue(1)), (float)Number(((DbDataReader)val6).GetValue(2)), (float)Number(((DbDataReader)val6).GetValue(3))));
}
}
((DbDataReader)val6).Close();
((DbCommand)val5).CommandText = "SELECT COUNT(*) FROM Stamps";
SqliteDataReader val7 = val5.ExecuteReader();
if (((DbDataReader)val7).Read())
{
stamps = (int)Number(((DbDataReader)val7).GetValue(0));
}
((DbDataReader)val7).Close();
}
finally
{
((DbConnection)val2).Close();
SqliteConnection.ClearAllPools();
}
return new SnapshotResult
{
SaveName = currentSaveFileName,
Data = File.ReadAllBytes(text),
Places = list,
Stamps = stamps,
FromScratch = flag,
Milliseconds = stopwatch.ElapsedMilliseconds
};
}
private static void CreateTables(SqliteCommand command)
{
Execute(command, SaveManager.CreateMetadataTable);
Execute(command, SaveManager.CreateEntityTable);
Execute(command, SaveManager.CreateStampsTable);
Execute(command, SaveManager.CreateGameGenericTable);
Execute(command, SaveManager.CreateGameProgressionGenericTable);
}
private static void CopyProgress(SqliteCommand command, string source)
{
Execute(command, "ATTACH DATABASE '" + Quote(source) + "' AS original");
try
{
Execute(command, "INSERT INTO Metadata (CreationGameVersion, SaveName) SELECT CreationGameVersion, SaveName FROM original.Metadata");
Execute(command, "INSERT INTO Generic (Key, ValueNumeric, ValueString, ValueBlob) SELECT Key, ValueNumeric, ValueString, ValueBlob FROM original.Generic");
Execute(command, "INSERT INTO ProgressionGeneric (Key, ValueNumeric, ValueString, ValueBlob) SELECT Key, ValueNumeric, ValueString, ValueBlob FROM original.ProgressionGeneric");
}
finally
{
Execute(command, "DETACH DATABASE original");
}
}
private static void AddMetadata(SqliteCommand command, SaveManager save)
{
string text = (Singleton<BootstrapManager>.HasInstance() ? Singleton<BootstrapManager>.Instance.GameVersion : string.Empty);
((DbCommand)command).CommandText = "INSERT INTO Metadata (CreationGameVersion, SaveName) VALUES ('" + Quote(text) + "', '" + Quote(save._currentSaveDisplayName) + "')";
((DbCommand)command).ExecuteNonQuery();
}
private static void Execute(SqliteCommand command, string text)
{
((DbCommand)command).CommandText = text;
((DbCommand)command).ExecuteNonQuery();
}
private static string Quote(string text)
{
return (text ?? string.Empty).Replace("'", "''");
}
private static double Number(Object value)
{
if (value == null)
{
return 0.0;
}
Type il2CppType = value.GetIl2CppType();
return ((il2CppType != null) ? ((MemberInfo)il2CppType).Name : null) switch
{
"DBNull" => 0.0,
"Int64" => ((Il2CppObjectBase)value).Unbox<long>(),
"Int32" => ((Il2CppObjectBase)value).Unbox<int>(),
"Double" => ((Il2CppObjectBase)value).Unbox<double>(),
"Single" => ((Il2CppObjectBase)value).Unbox<float>(),
_ => double.Parse(value.ToString(), CultureInfo.InvariantCulture),
};
}
}
}
namespace TooLate.Research
{
public sealed class MessageRecorder
{
public const double SummarySeconds = 5.0;
private readonly CatLogger _log;
private readonly Dictionary<string, int> _quiet = new Dictionary<string, int>(StringComparer.Ordinal);
private double _summaryAt;
public bool IsEnabled { get; private set; }
public MessageRecorder(CatLogger log)
{
_log = log;
}
public void SetEnabled(bool enabled)
{
if (IsEnabled != enabled)
{
if (!enabled)
{
Summarize();
}
IsEnabled = enabled;
_summaryAt = FrameLoop.Realtime + 5.0;
_log.Info(enabled ? "[Messages] Recording the game's network messages: every message once, moving and animation messages as counts every 5 seconds" : "[Messages] Recording stopped");
}
}
public void HostSent(int code, bool reliable, ulong target, Il2CppReferenceArray<Object> payload)
{
Record($"host -> {target}", code, reliable, payload);
}
public void ClientSent(int code, bool reliable, Il2CppReferenceArray<Object> payload)
{
Record("client -> host", code, reliable, payload);
}
public void Update()
{
if (IsEnabled && FrameLoop.Realtime >= _summaryAt)
{
Summarize();
_summaryAt = FrameLoop.Realtime + 5.0;
}
}
private void Record(string direction, int code, bool reliable, Il2CppReferenceArray<Object> payload)
{
if (MessageRoute.IsContinuous(code) || code == 44)
{
string key = direction + " " + Name(code);
_quiet[key] = ((!_quiet.TryGetValue(key, out var value)) ? 1 : (value + 1));
return;
}
_log.Info(string.Format(CultureInfo.InvariantCulture, "[Messages] {0:0.000} f{1} {2}: {3} ({4}){5}, {6}", FrameLoop.Realtime, FrameLoop.FrameCount, direction, Name(code), code, reliable ? string.Empty : " unreliable", GameProtocol.Describe(payload)));
}
private void Summarize()
{
if (_quiet.Count != 0)
{
_log.Info("[Messages] Last seconds: " + string.Join(", ", from pair in _quiet.OrderBy<KeyValuePair<string, int>, string>((KeyValuePair<string, int> pair) => pair.Key, StringComparer.Ordinal)
select $"{pair.Key} x{pair.Value}"));
_quiet.Clear();
}
}
public static string Name(int code)
{
//IL_002a: Unknown result type (might be due to invalid IL or missing references)
if (!Enum.IsDefined(typeof(ProtocolCode), code))
{
return "Code" + code;
}
return ((object)(ProtocolCode)code/*cast due to .constrained prefix*/).ToString();
}
}
}
namespace TooLate.Patches
{
public static class TooLatePatches
{
private static JoinCoordinator _joins;
private static LobbyOverflow _lobby;
private static MessageRecorder _recorder;
public static CatPatches Group { get; private set; }
public static CatPatches Install(string ownerId, JoinCoordinator joins, LobbyOverflow lobby, MessageRecorder recorder, CatLogger log)
{
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
_joins = joins;
_lobby = lobby;
_recorder = recorder;
Group = new CatPatches(ownerId, log).Prefix(typeof(Server), "SendMessage", new Type[4]
{
typeof(ProtocolCode),
typeof(bool),
typeof(ulong),
typeof(Il2CppReferenceArray<Object>)
}, typeof(TooLatePatches), "ServerSendPrefix").Prefix(typeof(Server), "BroadcastMessage", new Type[4]
{
typeof(ProtocolCode),
typeof(bool),
typeof(Nullable<ulong>),
typeof(Il2CppReferenceArray<Object>)
}, typeof(TooLatePatches), "ServerBroadcastPrefix").Prefix(typeof(Client), "SendMessage", new Type[3]
{
typeof(ProtocolCode),
typeof(bool),
typeof(Il2CppReferenceArray<Object>)
}, typeof(TooLatePatches), "ClientSendPrefix")
.Prefix(typeof(SaveManager), "NetworkManager_OnClientConnected", new Type[1] { typeof(ulong) }, typeof(TooLatePatches), "SaveOnConnectPrefix")
.Postfix(typeof(MultiplayerLobbyInterface), "ProcessClientJoin", new Type[1] { typeof(ulong) }, typeof(TooLatePatches), "LobbyJoinPostfix")
.Postfix(typeof(MultiplayerLobbyInterface), "NetworkManager_OnClientDisconnected", new Type[1] { typeof(ulong) }, typeof(TooLatePatches), "LobbyLeavePostfix");
Group.Apply();
return Group;
}
private static bool ServerSendPrefix(ProtocolCode __0, bool __1, ulong __2, Il2CppReferenceArray<Object> __3)
{
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
//IL_0018: Expected I4, but got Unknown
if (Group == null || !Group.IsActive)
{
return true;
}
try
{
int code = (int)__0;
if (_recorder.IsEnabled)
{
_recorder.HostSent(code, __1, __2, __3);
}
if (GameProtocol.IsSending)
{
return true;
}
_joins.Observe(code, __3);
return _joins.RouteOutgoing(code, __1, __2, __3);
}
catch (Exception ex)
{
Group.Fault("ServerSendPrefix", ex);
return true;
}
}
private static void ServerBroadcastPrefix(ProtocolCode __0, Il2CppReferenceArray<Object> __3)
{
//IL_0021: Unknown result type (might be due to invalid IL or missing references)
//IL_0028: Expected I4, but got Unknown
if (Group == null || !Group.IsActive || GameProtocol.IsSending)
{
return;
}
try
{
_joins.Observe((int)__0, __3);
}
catch (Exception ex)
{
Group.Fault("ServerBroadcastPrefix", ex);
}
}
private static void ClientSendPrefix(ProtocolCode __0, bool __1, Il2CppReferenceArray<Object> __2)
{
//IL_0033: Unknown result type (might be due to invalid IL or missing references)
//IL_003a: Expected I4, but got Unknown
//IL_0026: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Expected I4, but got Unknown
if (Group == null || !Group.IsActive)
{
return;
}
try
{
if (_recorder.IsEnabled)
{
_recorder.ClientSent((int)__0, __1, __2);
}
_joins.Observe((int)__0, __2);
}
catch (Exception ex)
{
Group.Fault("ClientSendPrefix", ex);
}
}
private static bool SaveOnConnectPrefix(ulong __0)
{
if (Group == null || !Group.IsActive)
{
return true;
}
try
{
return !_joins.TakesOverSave(__0);
}
catch (Exception ex)
{
Group.Fault("SaveOnConnectPrefix", ex);
return true;
}
}
private static void LobbyJoinPostfix(MultiplayerLobbyInterface __instance, ulong __0)
{
if (Group == null || !Group.IsActive)
{
return;
}
try
{
_lobby.OnJoined(__instance, __0);
}
catch (Exception ex)
{
Group.Fault("LobbyJoinPostfix", ex);
}
}
private static void LobbyLeavePostfix(MultiplayerLobbyInterface __instance, ulong __0)
{
if (Group == null || !Group.IsActive)
{
return;
}
try
{
_lobby.OnLeft(__instance, __0);
}
catch (Exception ex)
{
Group.Fault("LobbyLeavePostfix", ex);
}
}
}
}
namespace TooLate.Logic
{
public readonly struct EntityPlace
{
public uint Id { get; }
public float X { get; }
public float Y { get; }
public float Z { get; }
public EntityPlace(uint id, float x, float y, float z)
{
Id = id;
X = x;
Y = y;
Z = z;
}
public override string ToString()
{
return $"{Id} ({X:0.###}, {Y:0.###}, {Z:0.###})";
}
}
public static class EntityPlaces
{
public const uint EndMark = uint.MaxValue;
public static IReadOnlyList<EntityPlace> Merge(IEnumerable<EntityPlace> scene, IReadOnlyList<EntityPlace> saved)
{
HashSet<uint> hashSet = new HashSet<uint>(saved.Select((EntityPlace place) => place.Id));
List<EntityPlace> list = new List<EntityPlace>();
HashSet<uint> hashSet2 = new HashSet<uint>();
foreach (EntityPlace item in scene)
{
if (item.Id != 0 && item.Id != uint.MaxValue && !hashSet.Contains(item.Id) && hashSet2.Add(item.Id))
{
list.Add(item);
}
}
HashSet<uint> hashSet3 = new HashSet<uint>();
foreach (EntityPlace item2 in saved)
{
if (item2.Id != 0 && item2.Id != uint.MaxValue && hashSet3.Add(item2.Id))
{
list.Add(item2);
}
}
return list;
}
}
public readonly struct HostState
{
public bool Hosting { get; }
public bool LevelReady { get; }
public bool Restarting { get; }
public int TimePeriod { get; }
public HostState(bool hosting, bool levelReady, bool restarting, int timePeriod)
{
Hosting = hosting;
LevelReady = levelReady;
Restarting = restarting;
TimePeriod = timePeriod;
}
}
public static class JoinGate
{
public const int EveningRecap = 2;
public const int DawnRecap = 5;
public const double SettleSeconds = 3.0;
public static bool CanAccept(HostState state)
{
if (state.Hosting && state.LevelReady)
{
return !state.Restarting;
}
return false;
}
public static bool CanStart(HostState state)
{
if (CanAccept(state))
{
return !IsRecap(state.TimePeriod);
}
return false;
}
public static bool ReadyToStart(HostState state, double secondsConnected)
{
if (CanStart(state))
{
return secondsConnected >= 3.0;
}
return false;
}
public static bool IsRecap(int timePeriod)
{
if (timePeriod == 2 || timePeriod == 5)
{
return true;
}
return false;
}
}
public enum JoinStage
{
Waiting,
Loading,
Appearing,
Synced,
Playing
}
public readonly struct HeldMessage<T>
{
public int Code { get; }
public bool Reliable { get; }
public T Payload { get; }
public HeldMessage(int code, bool reliable, T payload)
{
Code = code;
Reliable = reliable;
Payload = payload;
}
}
public sealed class JoinTicket<T>
{
public const int MostHeldMessages = 20000;
private readonly List<HeldMessage<T>> _held = new List<HeldMessage<T>>();
public ulong ClientId { get; }
public string Name { get; }
public double ConnectedAt { get; }
public JoinStage Stage { get; private set; }
public double StageSince { get; private set; }
public IReadOnlyList<EntityPlace> Places { get; set; }
public int Dropped { get; private set; }
public int Overflowed { get; private set; }
public bool WaitingNoticeShown { get; set; }
public bool Flushing { get; set; }
public bool PlacesMerged { get; set; }
public bool StateSent { get; set; }
public IReadOnlyList<EntityPlace> ScenePlaces { get; set; }
public string FailReason { get; set; }
public IReadOnlyList<HeldMessage<T>> Held => _held;
public JoinTicket(ulong clientId, string name, double now)
{
ClientId = clientId;
Name = name;
ConnectedAt = now;
StageSince = now;
}
public void MoveTo(JoinStage stage, double now)
{
Stage = stage;
StageSince = now;
}
public Route Accept(int code, bool reliable, T payload)
{
Route route = MessageRoute.For(Stage, code);
switch (route)
{
case Route.Drop:
Dropped++;
break;
case Route.Hold:
Hold(code, reliable, payload);
break;
case Route.HoldLatest:
_held.RemoveAll((HeldMessage<T> message) => message.Code == code);
Hold(code, reliable, payload);
break;
}
return route;
}
public List<HeldMessage<T>> TakeHeld()
{
return TakeHeld(int.MaxValue);
}
public List<HeldMessage<T>> TakeHeld(int most)
{
int count = Math.Min(Math.Max(0, most), _held.Count);
List<HeldMessage<T>> range = _held.GetRange(0, count);
_held.RemoveRange(0, count);
return range;
}
private void Hold(int code, bool reliable, T payload)
{
if (_held.Count >= 20000)
{
Overflowed++;
}
else
{
_held.Add(new HeldMessage<T>(code, reliable, payload));
}
}
}
public readonly struct LastingMessage<T>
{
public int Code { get; }
public uint EntityId { get; }
public T Payload { get; }
public LastingMessage(int code, uint entityId, T payload)
{
Code = code;
EntityId = entityId;
Payload = payload;
}
}
public sealed class LastingMessages<T>
{
public const int MostMessages = 20000;
private readonly Dictionary<(int Code, uint EntityId), (long Order, LastingMessage<T> Message)> _latest = new Dictionary<(int, uint), (long, LastingMessage<T>)>();
private long _order;
public int Count => _latest.Count;
public static bool IsLasting(int code)
{
if ((uint)(code - 48) <= 2u)
{
return true;
}
return false;
}
public static bool IsPerEntity(int code)
{
return code != 50;
}
public bool Record(int code, uint entityId, T payload)
{
if (!IsLasting(code))
{
return false;
}
if (!IsPerEntity(code))
{
entityId = 0u;
}
else if (entityId == 0)
{
return false;
}
(int, uint) key = (code, entityId);
if (!_latest.ContainsKey(key) && _latest.Count >= 20000)
{
return false;
}
_latest[key] = (_order++, new LastingMessage<T>(code, entityId, payload));
return true;
}
public int Forget(uint entityId)
{
if (entityId == 0)
{
return 0;
}
List<(int, uint)> list = _latest.Keys.Where(((int Code, uint EntityId) key) => key.EntityId == entityId).ToList();
foreach (var item in list)
{
_latest.Remove(item);
}
return list.Count;
}
public void Clear()
{
_latest.Clear();
_order = 0L;
}
public IReadOnlyList<LastingMessage<T>> For(Func<uint, bool> known)
{
return (from entry in _latest.Values
where entry.Message.EntityId == 0 || known == null || known(entry.Message.EntityId)
orderby entry.Order
select entry.Message).ToList();
}
}
public enum Route
{
Pass,
Drop,
Hold,
HoldLatest,
Replace
}
public static class MessageRoute
{
public static Route For(JoinStage stage, int code)
{
if (code == 21)
{
return Route.Replace;
}
bool flag = (uint)(stage - 3) <= 1u;
if (flag || IsJoinFlow(code))
{
return Route.Pass;
}
if (stage == JoinStage.Appearing && code == 20)
{
return Route.Pass;
}
flag = IsContinuous(code);
if (!flag)
{
bool flag2 = ((code == 20 || code == 38 || code == 51) ? true : false);
flag = flag2;
}
if (flag)
{
return Route.Drop;
}
if (stage == JoinStage.Waiting)
{
return Route.Drop;
}
if (code != 44)
{
return Route.Hold;
}
return Route.HoldLatest;
}
public static bool IsJoinFlow(int code)
{
switch (code)
{
case 3:
case 4:
case 5:
case 6:
case 9:
case 10:
case 11:
case 13:
case 28:
case 29:
return true;
default:
return false;
}
}
public static bool IsContinuous(int code)
{
if ((uint)(code - 22) <= 1u || (uint)(code - 26) <= 1u)
{
return true;
}
return false;
}
}
public static class PlayerLimit
{
public const int GamePlayers = 4;
public const int MostPlayers = 8;
public const string CountPattern = "E8 ?? ?? ?? ?? 83 F8 05 0F 8D";
public const int CountOffset = 7;
public const string LoadingPattern = "44 38 A1 90 00 00 00 0F 85 ?? ?? ?? ??";
public const int LoadingOffset = 7;
public const int LoadingLength = 6;
public static readonly byte[] SkipJump = new byte[6] { 102, 15, 31, 68, 0, 0 };
public static int Clamp(int players)
{
return Math.Max(4, Math.Min(8, players));
}
public static byte ConnectionLimit(int players)
{
return (byte)(Clamp(players) + 1);
}
}
public static class ProtocolCodes
{
public const int ConnectAck = 3;
public const int ClientConnected = 4;
public const int ClientDisconnect = 5;
public const int ClientDisconnectBroadcast = 6;
public const int ServerDisconnect = 9;
public const int ServerGameStart = 10;
public const int ServerFinalizeLoad = 11;
public const int GrantPlayerSpawn = 13;
public const int EntityDisposed = 18;
public const int PlayerSpawnedBroadcast = 20;
public const int IdentifiersSynchronization = 21;
public const int MovementAndRotation = 22;
public const int MovementAndRotationBroadcast = 23;
public const int AnimationParameter = 26;
public const int AnimationParameterBroadcast = 27;
public const int PlayerName = 28;
public const int PlayerNameBroadcast = 29;
public const int GenericMessage = 38;
public const int GameTimeSynchronization = 44;
public const int GameTimePeriod = 45;
public const int SetInteractionsLocked = 48;
public const int SetParcelDamaged = 49;
public const int BoatDestinations = 50;
public const int SaveSynchronization = 51;
}
public readonly struct StateMessage
{
public uint EntityId { get; }
public string Text { get; }
public string Kind { get; }
public StateMessage(uint entityId, string text, string kind)
{
EntityId = entityId;
Text = text;
Kind = kind;
}
}
public sealed class StateLedger
{
public const int MostMessages = 20000;
public const string CounterKind = "CustomerCounter";
public const string CounterOpenKind = "CustomerCounter;OpenState";
private static readonly char[] Separators = new char[2] { ';', ':' };
private static readonly HashSet<string> MomentKinds = new HashSet<string>(StringComparer.Ordinal) { "Captain", "Customer", "CustomerAudio", "CustomerAppearance", "CarryParcel" };
private readonly Dictionary<(uint EntityId, string Kind), (long Order, StateMessage Message)> _latest = new Dictionary<(uint, string), (long, StateMessage)>();
private long _order;
public int Count => _latest.Count;
public static string KindOf(string text)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
int num = text.IndexOfAny(Separators);
if (num < 0)
{
return text;
}
string text2 = text.Substring(0, num);
if (text2 != "CustomerCounter")
{
return text2;
}
int num2 = text.IndexOfAny(Separators, num + 1);
if (num2 >= 0)
{
return text.Substring(0, num2);
}
return text;
}
public static bool IsLasting(string kind)
{
if (string.IsNullOrEmpty(kind) || MomentKinds.Contains(kind))
{
return false;
}
if (kind.StartsWith("CustomerCounter", StringComparison.Ordinal))
{
return kind == "CustomerCounter;OpenState";
}
return true;
}
public bool Record(uint entityId, string text)
{
if (entityId == 0 || text == null)
{
return false;
}
string text2 = KindOf(text);
if (!IsLasting(text2))
{
return false;
}
(uint, string) key = (entityId, text2);
if (!_latest.ContainsKey(key) && _latest.Count >= 20000)
{
return false;
}
_latest[key] = (_order++, new StateMessage(entityId, text, text2));
return true;
}
public int Forget(uint entityId)
{
List<(uint, string)> list = _latest.Keys.Where(((uint EntityId, string Kind) key) => key.EntityId == entityId).ToList();
foreach (var item in list)
{
_latest.Remove(item);
}
return list.Count;
}
public void Clear()
{
_latest.Clear();
_order = 0L;
}
public IReadOnlyList<StateMessage> For(Func<uint, bool> known)
{
return (from entry in _latest.Values
where known == null || known(entry.Message.EntityId)
orderby entry.Order
select entry.Message).ToList();
}
}
}