Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LateJoin v1.0.0
BepInEx/plugins/RepoLateJoin.dll
Decompiled 3 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using RepoLateJoin.Client; using RepoLateJoin.Core; using RepoLateJoin.Join; using RepoLateJoin.Lobby; using RepoLateJoin.Menu; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("RepoLateJoin")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Unified late-join mod for R.E.P.O.")] [assembly: AssemblyFileVersion("1.5.3.0")] [assembly: AssemblyInformationalVersion("1.5.3")] [assembly: AssemblyProduct("RepoLateJoin")] [assembly: AssemblyTitle("RepoLateJoin")] [assembly: AssemblyVersion("1.5.3.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 RepoLateJoin { [BepInPlugin("repolatejoin.unified", "RepoLateJoin", "1.5.3")] public sealed class Plugin : BaseUnityPlugin { private sealed class Runner : MonoBehaviour { private void Update() { if (PunGate.Ready) { LobbyGate.Tick(); LoadingWatchdog.Tick(); ModuleConnectionSync.TickClientRepair(); } } private void OnGUI() { StatusOverlay.Draw(); } } internal const string Guid = "repolatejoin.unified"; private static readonly Dictionary<string, string> Superseded = new Dictionary<string, string> { { "ozoka.latejoin", "LateJoin (Ozoka Gaming)" }, { "chaos.holz.laterepo", "LateRepo (Chaosholz)" }, { "laterepo.fix.runtime", "LateRepo Fix (AST Studios)" }, { "jp.laterepo.introfix", "LateRepo Intro Fix (CapackMods)" }, { "knightmike.latejoin", "Late Join (knightmike)" }, { "zichen.latejoinnow", "LateJoinNow (Zichen)" } }; private Harmony _harmony; private void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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_0034: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown Log.Attach(((BaseUnityPlugin)this).Logger); ModConfig.Bind(((BaseUnityPlugin)this).Config); WarnAboutSupersededMods(); GameObject val = new GameObject("RepoLateJoin") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); JoinQueue.Attach((MonoBehaviour)(object)val.AddComponent<Runner>()); _harmony = new Harmony("repolatejoin.unified"); int num = ApplyPatches(); ClientGuards.Install(_harmony); Log.Info("RepoLateJoin ready: " + num + " patch group(s) active. Late join is " + (ModConfig.On ? "on" : "off") + "."); } private int ApplyPatches() { Type[] array; try { array = (from t in typeof(Plugin).Assembly.GetTypes() where t.Namespace == "RepoLateJoin.Patches" && t.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Length != 0 select t).ToArray(); } catch (Exception ex) { Log.Error("Could not enumerate patch classes: " + ex); return 0; } int num = 0; Type[] array2 = array; foreach (Type type in array2) { try { _harmony.CreateClassProcessor(type).Patch(); num++; Log.Verbose("Applied patch group " + type.Name + "."); } catch (Exception ex2) { Log.Warn("Patch group " + type.Name + " could not be applied (" + ex2.GetType().Name + ": " + ex2.Message + "). That feature is disabled; everything else still works."); } } return num; } private void WarnAboutSupersededMods() { List<string> list = new List<string>(); try { foreach (KeyValuePair<string, string> item in Superseded) { if (Chainloader.PluginInfos.ContainsKey(item.Key)) { list.Add(item.Value); } } } catch (Exception ex) { Log.Verbose("Could not inspect the loaded plugin list: " + ex.GetType().Name); return; } if (list.Count != 0) { Log.Warn("RepoLateJoin already contains the features of: " + string.Join(", ", list) + ". Remove " + ((list.Count == 1) ? "it" : "them") + " - running both leaves two mods fighting over the same lobby state."); } } } } namespace RepoLateJoin.Patches { [HarmonyPatch(typeof(PlayerDeathHead), "UpdateParticleColors")] internal static class DeathHeadColorPatch { private static Exception Finalizer(Exception __exception) { if (__exception == null) { return null; } if (!ModConfig.On) { return __exception; } Log.Once("deathheadcolors", "Suppressed a death head colour update that ran before the head finished arriving over the network (" + __exception.GetType().Name + "). Particle colours stay default until the next cosmetics change."); return null; } } [HarmonyPatch] internal static class EnemyOnScreenLogicPatch { private static FieldInfo _thisField; private static MethodBase TargetMethod() { Type type = EnemyAwareness.FindIteratorType(typeof(EnemyOnScreen), "Logic"); if (type == null) { return null; } _thisField = Reflect.Field(type, "<>4__this"); return Reflect.Method(type, "MoveNext"); } private static void Prefix(object __instance) { EnemyAwareness.EnsureFor(EnemyAwareness.OwnerOf<EnemyOnScreen>(__instance, _thisField)); } } [HarmonyPatch] internal static class EnemyVisionLogicPatch { private static FieldInfo _thisField; private static MethodBase TargetMethod() { Type type = EnemyAwareness.FindIteratorType(typeof(EnemyVision), "Vision"); if (type == null) { return null; } _thisField = Reflect.Field(type, "<>4__this"); return Reflect.Method(type, "MoveNext"); } private static void Prefix(object __instance) { EnemyAwareness.EnsureFor(EnemyAwareness.OwnerOf<EnemyVision>(__instance, _thisField)); } } [HarmonyPatch(typeof(RunManager), "ChangeLevel")] internal static class RunManagerChangeLevelPatch { private static readonly FieldInfo RestartingField = Reflect.Field(typeof(RunManager), "restarting"); private static void Prefix() { if (!PunGate.Ready) { return; } try { if ((!SemiFunc.MenuLevel() && !SemiFunc.IsMasterClientOrSingleplayer()) || ((Object)(object)RunManager.instance != (Object)null && Reflect.Get(RestartingField, RunManager.instance, fallback: false))) { return; } } catch (Exception ex) { Log.Verbose("Could not evaluate the change-level guard: " + ex.GetType().Name); return; } LobbyGate.BeginTransition(); PhotonCacheCleaner.ClearSceneCache(); PhotonCacheCleaner.ClearStaleLevelUpdateRpc(); ModuleConnectionSync.Clear(); LevelStateSync.Clear(); JoinQueue.Reset(); ClientGuards.Disarm(); LoadingWatchdog.Reset(); } } [HarmonyPatch(typeof(LevelGenerator), "Start")] internal static class LevelGeneratorStartPatch { private static void Prefix(LevelGenerator __instance) { PunGate.Mark(); if (ModConfig.On) { PhotonCacheCleaner.ClearStaleGenerationRpcs(__instance); } } private static Exception Finalizer(Exception __exception) { if (__exception == null) { return null; } if (!IsSingleplayerPoolMiss(__exception)) { return __exception; } Log.Once("sppool", "Suppressed an outdated RunManager.singleplayerPool access during level generation."); return null; } private static bool IsSingleplayerPoolMiss(Exception e) { while (e != null) { if (e is MissingFieldException && e.ToString().IndexOf("singleplayerPool", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } e = e.InnerException; } return false; } } [HarmonyPatch(typeof(LoadingUI), "LevelAnimationStart")] internal static class LoadingUiAnimationStartPatch { private static void Prefix() { LoadingWatchdog.EnsureLevelReference(); } } [HarmonyPatch(typeof(MenuPageLobby), "ButtonStart")] internal static class MenuPageLobbyButtonStartPatch { private static void Postfix() { if (ModConfig.On) { LobbyGate.ResetAll(); Log.Verbose("Lobby start pressed; join gating state reset."); } } } [HarmonyPatch(typeof(PlayerAvatar), "Spawn")] internal static class PlayerAvatarSpawnPatch { private static readonly FieldInfo SpawnedField = Reflect.Field(typeof(PlayerAvatar), "spawned"); private static bool Prefix(PlayerAvatar __instance) { if (!ModConfig.On || (Object)(object)__instance == (Object)null) { return true; } if (!Reflect.Get(SpawnedField, __instance, fallback: false)) { return true; } Log.Once("dupespawn", "Blocked a re-spawn of an already-spawned player so late joins do not teleport the party."); return false; } } [HarmonyPatch(typeof(PlayerAvatar), "Start")] internal static class PlayerAvatarStartPatch { private static void Postfix(PlayerAvatar __instance) { if (ModConfig.On && !((Object)(object)__instance == (Object)null)) { EnemyAwareness.RegisterAllPlayers(); if (PunGate.Ready && !((Object)(object)__instance.photonView == (Object)null) && __instance.photonView.IsMine && !PhotonNetwork.IsMasterClient) { ClientGuards.EvaluateLocalState(__instance); JoinQueue.ReleaseOwnLoadingScreen(__instance); } } } } [HarmonyPatch(typeof(SteamManager), "LockLobby")] internal static class SteamManagerLockLobbyPatch { private static bool Prefix() { if (!PunGate.Ready) { return true; } if (LobbyGate.SelfLocking) { return true; } if (!LobbyGate.JoinAllowedNow()) { return true; } Log.Once("keepopen", "Kept the lobby unlocked because late joining is currently allowed."); LobbyGate.Apply("lock suppressed", force: true); return false; } } [HarmonyPatch(typeof(NetworkManager), "OnEnable")] internal static class NetworkManagerOnEnablePatch { private static void Postfix() { PunGate.Mark(); if (ModConfig.On) { ModuleConnectionSync.StartListening(); } } } [HarmonyPatch(typeof(NetworkManager), "OnDisable")] internal static class NetworkManagerOnDisablePatch { private static void Postfix() { ModuleConnectionSync.StopListening(); } } [HarmonyPatch(typeof(NetworkManager), "OnPlayerEnteredRoom")] internal static class NetworkManagerPlayerEnteredPatch { private static void Postfix(Player newPlayer) { if (PunGate.Ready) { JoinQueue.OnPlayerEntered(newPlayer); } } } [HarmonyPatch(typeof(NetworkManager), "OnPlayerLeftRoom")] internal static class NetworkManagerPlayerLeftPatch { private static void Postfix(Player otherPlayer) { if (PunGate.Ready) { JoinQueue.OnPlayerLeft(otherPlayer); } } } [HarmonyPatch(typeof(Module), "ModuleConnectionSet")] internal static class ModuleConnectionSetPatch { private static void Postfix(Module __instance, bool _top, bool _bottom, bool _right, bool _left, bool _first) { ModuleConnectionSync.Remember(__instance, _top, _bottom, _right, _left, _first); } } [HarmonyPatch(typeof(EnemyOnScreen), "OnEnable")] internal static class EnemyOnScreenEnablePatch { private static void Prefix(EnemyOnScreen __instance) { EnemyAwareness.EnsureFor(__instance); } } [HarmonyPatch(typeof(EnemyVision), "OnEnable")] internal static class EnemyVisionEnablePatch { private static void Prefix(EnemyVision __instance) { EnemyAwareness.EnsureFor(__instance); } } [HarmonyPatch] internal static class GrabObjectDestroyPatch { private static IEnumerable<MethodBase> TargetMethods() { string[] array = new string[2] { "PhysGrabObject", "StaticGrabObject" }; for (int i = 0; i < array.Length; i++) { MethodInfo methodInfo = Reflect.Method(Reflect.TypeByName(array[i]), "DestroyPhysGrabObjectRPC"); if (methodInfo != null) { yield return methodInfo; } } } private static void Prefix(object __instance) { if (!PunGate.Ready || !PhotonNetwork.IsMasterClient) { return; } Component val = (Component)((__instance is Component) ? __instance : null); if (val != null && !((Object)(object)val == (Object)null)) { PhotonView component = val.GetComponent<PhotonView>(); LevelStateSync.RememberDestroyed(component); if ((Object)(object)component != (Object)null && component.ViewID == component.InstantiationId && PhotonCacheCleaner.ClearOne(component)) { Log.Once("destroycache", "Clearing the Photon cache entry of destroyed objects so late joiners do not receive loot that was already hauled in."); } } } } [HarmonyPatch(typeof(MenuPageEsc), "Start")] internal static class MenuPageEscStartPatch { private static void Postfix(MenuPageEsc __instance) { InviteButton.TryAdd(__instance); } } [HarmonyPatch(typeof(GumballValuable), "UpdateHypnosisLinesList")] internal static class GumballValuableHypnosisPatch { private static readonly FieldInfo LinesField = Reflect.Field(typeof(GumballValuable), "hypnosisLines"); private static readonly FieldInfo InSightField = Reflect.Field(typeof(GumballValuable), "playerInSight"); private static readonly FieldInfo SeenField = Reflect.Field(typeof(GumballValuable), "previouslySeenList"); private static void Prefix(GumballValuable __instance) { if (!ModConfig.On || (Object)(object)__instance == (Object)null) { return; } try { List<PlayerAvatar> list = SemiFunc.PlayerGetList(); if (list != null) { GrowFlags(InSightField, __instance, list.Count); GrowFlags(SeenField, __instance, list.Count); GrowLines(__instance, list); } } catch (Exception ex) { Log.Verbose("Could not grow the gumball hypnosis lists: " + ex.GetType().Name); } } private static void GrowFlags(FieldInfo field, GumballValuable instance, int target) { if (field?.GetValue(instance) is List<bool> list) { while (list.Count < target) { list.Add(item: false); } } } private static void GrowLines(GumballValuable instance, List<PlayerAvatar> players) { //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) if (!(LinesField?.GetValue(instance) is List<GameObject> list) || list.Count >= players.Count) { return; } GameObject hypnosisLine = instance.hypnosisLine; Transform hypnosisLinesParenTransform = instance.HypnosisLinesParenTransform; if ((Object)(object)hypnosisLine == (Object)null) { return; } int num = 0; for (int i = list.Count; i < players.Count; i++) { PlayerAvatar val = players[i]; if ((Object)(object)val == (Object)null) { list.Add(null); continue; } GameObject val2 = Object.Instantiate<GameObject>(hypnosisLine, ((Component)instance).transform.position, Quaternion.identity); Transform val3 = null; try { PlayerLocalCamera localCamera = val.localCamera; val3 = ((localCamera != null) ? localCamera.GetOverrideTransform() : null); } catch { } if ((Object)(object)val3 != (Object)null) { val2.transform.SetParent(val3); val2.transform.localPosition = Vector3.zero; val2.transform.localRotation = Quaternion.identity; } EyeLines component = val2.GetComponent<EyeLines>(); if ((Object)(object)component != (Object)null) { component.InitializeLine(val); } if ((Object)(object)hypnosisLinesParenTransform != (Object)null) { val2.transform.SetParent(hypnosisLinesParenTransform); } val2.SetActive(false); list.Add(val2); num++; } if (num > 0) { Log.Once("gumball", "Grew a gumball valuable's hypnosis line list by " + num + " so it stops throwing once somebody joins mid-level."); } } } [HarmonyPatch(typeof(RunManager), "UpdateLevel")] internal static class InventorySlotPatch { private static void Postfix() { if (!ModConfig.On) { return; } try { if ((Object)(object)Inventory.instance == (Object)null || SemiFunc.MenuLevel() || SemiFunc.RunIsArena()) { return; } int num = 0; InventorySpot[] array = Object.FindObjectsOfType<InventorySpot>(true); foreach (InventorySpot val in array) { if (!((Object)(object)Inventory.instance.GetSpotByIndex(val.inventorySpotIndex) != (Object)null)) { Inventory.instance.InventorySpotAddAtIndex(val, val.inventorySpotIndex); num++; } } if (num > 0) { Log.Info("Registered " + num + " inventory slot(s) the join skipped."); } } catch (Exception ex) { Log.Verbose("Could not register inventory slots: " + ex.GetType().Name); } } } [HarmonyPatch(typeof(LevelPoint), "Start")] internal static class LevelPointLinkPatch { private static void Postfix(LevelPoint __instance) { if (ModConfig.On && __instance.ModuleConnect && !((Object)(object)LevelGenerator.Instance == (Object)null) && LevelGenerator.Instance.Generated) { ((MonoBehaviour)__instance).StartCoroutine(Relink(__instance)); } } private static IEnumerator Relink(LevelPoint point) { yield return null; if ((Object)(object)point == (Object)null || (Object)(object)LevelGenerator.Instance == (Object)null) { yield break; } Transform transform = ((Component)point).transform; foreach (LevelPoint levelPathPoint in LevelGenerator.Instance.LevelPathPoints) { if ((Object)(object)levelPathPoint == (Object)null || (Object)(object)levelPathPoint == (Object)(object)point || !levelPathPoint.ModuleConnect || point.ConnectedPoints.Contains(levelPathPoint)) { continue; } Transform transform2 = ((Component)levelPathPoint).transform; if (Vector3.Distance(transform.position, transform2.position) < 15f && Vector3.Dot(transform2.forward, transform.forward) <= -0.8f) { Vector3 forward = transform2.forward; Vector3 val = transform.position - transform2.position; if (Vector3.Dot(forward, ((Vector3)(ref val)).normalized) > 0.8f) { point.ConnectedPoints.Add(levelPathPoint); } } } } } [HarmonyPatch] internal static class LightManagerActivationPatch { private static readonly HashSet<string> Reported = new HashSet<string>(); private static IEnumerable<MethodBase> TargetMethods() { Type type = Reflect.TypeByName("LightManager"); string[] array = new string[2] { "HandleLightActivation", "HandleEmissionActivation" }; foreach (string name in array) { MethodInfo methodInfo = Reflect.Method(type, name); if (methodInfo != null) { yield return methodInfo; } } } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod) { if (__exception == null) { return null; } if (!ModConfig.On) { return __exception; } string text = __originalMethod?.Name ?? "?"; if (Reported.Add(text)) { Log.Info("Suppressed a failure in LightManager." + text + " (" + __exception.GetType().Name + "). Without this one prop light aborts the whole lighting pass and the rest of the level's decorations stay dark."); } else { Log.Verbose("Suppressed another LightManager." + text + " failure."); } return null; } } } namespace RepoLateJoin.Menu { internal static class InviteButton { [CompilerGenerated] private static class <>O { public static UnityAction <0>__OpenInviteOverlay; } private const string CloneName = "RepoLateJoin_InviteButton"; private static readonly FieldInfo OriginalPositionField = Reflect.Field(typeof(MenuButton), "originalButtonPosition"); internal static void TryAdd(MenuPageEsc page) { //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Expected O, but got Unknown if (!PunGate.Ready || !ModConfig.On || (Object)(object)page == (Object)null) { return; } if (!PhotonNetwork.InRoom || !LobbyGate.SceneAllowsJoin()) { Log.Verbose("Skipping the invite button: this scene does not accept joins."); return; } try { MenuButton val = FindQuitButton(page); if ((Object)(object)val == (Object)null) { Log.Once("noanchor", "Could not find the quit button to anchor the invite button to."); return; } Transform parent = ((Component)val).transform.parent; if ((Object)(object)parent == (Object)null || (Object)(object)parent.Find("RepoLateJoin_InviteButton") != (Object)null) { return; } GameObject val2 = Object.Instantiate<GameObject>(((Component)val).gameObject, parent); ((Object)val2).name = "RepoLateJoin_InviteButton"; MenuButtonPopUp component = val2.GetComponent<MenuButtonPopUp>(); if ((Object)(object)component != (Object)null) { Object.Destroy((Object)(object)component); } Relabel(val2, "INVITE"); Button component2 = val2.GetComponent<Button>(); if ((Object)(object)component2 != (Object)null) { ((Behaviour)component2).enabled = true; ((Selectable)component2).interactable = true; ((UnityEventBase)component2.onClick).RemoveAllListeners(); ButtonClickedEvent onClick = component2.onClick; object obj = <>O.<0>__OpenInviteOverlay; if (obj == null) { UnityAction val3 = OpenInviteOverlay; <>O.<0>__OpenInviteOverlay = val3; obj = (object)val3; } ((UnityEvent)onClick).AddListener((UnityAction)obj); } val2.transform.SetSiblingIndex(((Component)val).transform.GetSiblingIndex()); ((MonoBehaviour)page).StartCoroutine(PlaceNextFrame(val2, ((Component)val).gameObject)); Log.Once("invite", "Added an INVITE button to the escape menu."); } catch (Exception ex) { Log.Verbose("Could not add the invite button: " + ex.GetType().Name); } } private static void OpenInviteOverlay() { try { SteamManager instance = SteamManager.instance; if (instance != null) { instance.OpenSteamOverlayToInvite(); } } catch (Exception ex) { Log.Verbose("Could not open the Steam invite overlay: " + ex.GetType().Name); } } private static IEnumerator PlaceNextFrame(GameObject clone, GameObject anchor) { yield return null; if ((Object)(object)clone == (Object)null || (Object)(object)anchor == (Object)null) { yield break; } RectTransform component = clone.GetComponent<RectTransform>(); RectTransform component2 = anchor.GetComponent<RectTransform>(); if ((Object)(object)component == (Object)null || (Object)(object)component2 == (Object)null) { yield break; } component.anchorMin = component2.anchorMin; component.anchorMax = component2.anchorMax; component.pivot = component2.pivot; component.sizeDelta = component2.sizeDelta; ((Transform)component).localScale = ((Transform)component2).localScale; ((Transform)component).localPosition = FreeSlotBelow(clone, component2); Relabel(clone, "INVITE"); MenuButton component3 = clone.GetComponent<MenuButton>(); if (!((Object)(object)component3 != (Object)null)) { yield break; } Reflect.Set(OriginalPositionField, component3, Vector2.op_Implicit(((Transform)component).localPosition)); try { component3.ResizeToText(); } catch (Exception ex) { Log.Verbose("Invite button resize failed: " + ex.GetType().Name); } } private static Vector3 FreeSlotBelow(GameObject clone, RectTransform anchor) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) List<float> list = new List<float>(); foreach (Transform item in ((Transform)anchor).parent) { Transform val = item; if ((Object)(object)((Component)val).gameObject != (Object)(object)clone && ((Component)val).gameObject.activeSelf && (Object)(object)((Component)val).GetComponent<MenuButton>() != (Object)null) { list.Add(val.localPosition.y); } } list.Sort(); float num = float.MaxValue; for (int i = 1; i < list.Count; i++) { float num2 = list[i] - list[i - 1]; if (num2 > 1f && num2 < num) { num = num2; } } if (num == float.MaxValue) { num = 32f; } float num3 = ((list.Count > 0) ? list[0] : ((Transform)anchor).localPosition.y); return new Vector3(((Transform)anchor).localPosition.x, num3 - num, ((Transform)anchor).localPosition.z); } private static void Relabel(GameObject buttonObject, string label) { if ((Object)(object)buttonObject == (Object)null) { return; } MenuButton component = buttonObject.GetComponent<MenuButton>(); if ((Object)(object)component != (Object)null) { component.buttonTextString = label; } TextMeshProUGUI[] componentsInChildren = buttonObject.GetComponentsInChildren<TextMeshProUGUI>(true); foreach (TextMeshProUGUI val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { ((TMP_Text)val).SetText(label, true); ((TMP_Text)val).ForceMeshUpdate(false, false); } } } private static MenuButton FindQuitButton(MenuPageEsc page) { MenuButton[] componentsInChildren = ((Component)page).GetComponentsInChildren<MenuButton>(true); MenuButton val = null; MenuButton[] array = componentsInChildren; foreach (MenuButton val2 in array) { if (!((Object)(object)val2 == (Object)null)) { string text = (val2.buttonTextString ?? string.Empty).ToUpperInvariant().Replace(" ", string.Empty); if (text == "QUITGAME") { return val2; } if ((Object)(object)val == (Object)null && text.Contains("QUIT")) { val = val2; } } } return val; } } internal static class StatusOverlay { private static GUIStyle _style; private static GUIStyle _shadow; internal static void Draw() { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Expected O, but got Unknown //IL_00da: 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_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.ShowStatus == null || !ModConfig.ShowStatus.Value || !PunGate.PhotonInitialised || !PhotonNetwork.InRoom) { return; } Room currentRoom = PhotonNetwork.CurrentRoom; if (currentRoom == null) { return; } string text = currentRoom.Name; try { if (((Dictionary<object, object>)(object)((RoomInfo)currentRoom).CustomProperties).TryGetValue((object)"server_name", out object value) && value is string text2 && !string.IsNullOrEmpty(text2)) { text = text2; } } catch (Exception) { } bool flag = currentRoom.IsOpen && ModConfig.On; string text3 = "Room: " + text + "\nLate join: " + (flag ? "OPEN" : "CLOSED"); if (_style == null) { _style = new GUIStyle(GUI.skin.label) { fontSize = 16, fontStyle = (FontStyle)1 }; _shadow = new GUIStyle(_style); _shadow.normal.textColor = Color.black; } _style.normal.textColor = (flag ? new Color(0.5f, 1f, 0.5f) : new Color(1f, 0.5f, 0.5f)); Rect val = default(Rect); ((Rect)(ref val))..ctor(12f, 10f, 600f, 50f); GUI.Label(new Rect(((Rect)(ref val)).x + 1f, ((Rect)(ref val)).y + 1f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), text3, _shadow); GUI.Label(val, text3, _style); } } } namespace RepoLateJoin.Lobby { internal static class LobbyGate { private static readonly FieldInfo PrivateLobbyField = Reflect.Field(typeof(SteamManager), "privateLobby"); private static readonly FieldInfo LobbyTypeField = Reflect.Field(typeof(GameManager), "lobbyType"); private const float TransitionCooldown = 4f; private static float _cooldownUntil; private static float _nextTick; private static bool? _appliedState; internal static bool SelfLocking; internal static bool InTransition => Time.unscaledTime < _cooldownUntil; internal static bool SceneAllowsJoin() { try { if (SemiFunc.RunIsLobbyMenu()) { return true; } if (SemiFunc.RunIsLobby()) { return ModConfig.JoinInLobby.Value; } if (SemiFunc.RunIsShop()) { return ModConfig.JoinInShop.Value; } if (SemiFunc.RunIsLevel()) { return ModConfig.JoinInLevel.Value; } return false; } catch (Exception ex) { Log.Verbose("Scene gate could not be evaluated: " + ex.GetType().Name); return false; } } internal static bool ReadyForJoiners() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: 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_003b: Invalid comparison between Unknown and I4 //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 try { if ((Object)(object)LevelGenerator.Instance == (Object)null || !LevelGenerator.Instance.Generated) { return false; } if ((Object)(object)GameDirector.instance == (Object)null) { return false; } gameState currentState = GameDirector.instance.currentState; return (int)currentState == 2 || (int)currentState == 7; } catch (Exception ex) { Log.Verbose("Ready gate could not be evaluated: " + ex.GetType().Name); return false; } } internal static bool JoinAllowedNow() { if (PunGate.Ready && PhotonNetwork.IsMasterClient && PhotonNetwork.InRoom && !InTransition && SceneAllowsJoin()) { return ReadyForJoiners(); } return false; } internal static void BeginTransition() { _appliedState = null; _cooldownUntil = Time.unscaledTime + 4f; Apply("level transition started", force: true); } internal static void Tick() { if (PunGate.Ready && !(Time.unscaledTime < _nextTick)) { _nextTick = Time.unscaledTime + 1f; Apply("periodic refresh"); } } internal static void Apply(string reason, bool force = false) { if (!PunGate.Ready || !PhotonNetwork.IsMasterClient || !PhotonNetwork.InRoom) { return; } bool flag = JoinAllowedNow(); if (force || !_appliedState.HasValue || _appliedState.Value != flag) { _appliedState = flag; if (flag) { OpenRoom(reason); } else { CloseRoom(reason); } } } private static void OpenRoom(string reason) { bool flag = !IsPrivateLobby(); try { Room currentRoom = PhotonNetwork.CurrentRoom; if (currentRoom != null) { currentRoom.IsOpen = true; currentRoom.IsVisible = flag; } if ((Object)(object)SteamManager.instance != (Object)null) { SteamManager.instance.UnlockLobby(flag); } if (flag && IsMatchmaking() && (Object)(object)GameManager.instance != (Object)null) { GameManager.instance.SetConnectRandom(true); } Log.Once("open:" + reason, "Lobby open for late joins (" + reason + "), advertised=" + flag + "."); } catch (Exception ex) { Log.Verbose("Could not open the lobby after " + reason + ": " + ex.GetType().Name); } } private static void CloseRoom(string reason) { try { Room currentRoom = PhotonNetwork.CurrentRoom; if (currentRoom != null) { currentRoom.IsOpen = false; currentRoom.IsVisible = false; } if (IsMatchmaking() && (Object)(object)GameManager.instance != (Object)null) { GameManager.instance.SetConnectRandom(false); } if ((Object)(object)SteamManager.instance != (Object)null) { SelfLocking = true; try { SteamManager.instance.LockLobby(); } finally { SelfLocking = false; } } Log.Once("close:" + reason, "Lobby closed to new joins (" + reason + ")."); } catch (Exception ex) { Log.Verbose("Could not close the lobby after " + reason + ": " + ex.GetType().Name); } } internal static bool IsPrivateLobby() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 LobbyTypes? val = LobbyType(); if (val.HasValue) { return (int)val.Value == 0; } if ((Object)(object)SteamManager.instance != (Object)null && PrivateLobbyField != null) { return Reflect.Get(PrivateLobbyField, SteamManager.instance, fallback: true); } return true; } internal static bool IsMatchmaking() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 LobbyTypes? val = LobbyType(); if (val.HasValue) { return (int)val.Value == 2; } return false; } private static LobbyTypes? LobbyType() { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)GameManager.instance == (Object)null || LobbyTypeField == null) { return null; } try { object value = LobbyTypeField.GetValue(GameManager.instance); if (value is LobbyTypes) { return (LobbyTypes)value; } } catch { } return null; } internal static void ResetAll() { _cooldownUntil = 0f; _appliedState = null; } } internal static class PhotonCacheCleaner { private static readonly FieldInfo RemoveFilterField = Reflect.Field(typeof(PhotonNetwork), "removeFilter"); private static readonly FieldInfo KeyByteSevenField = Reflect.Field(typeof(PhotonNetwork), "keyByteSeven"); private static readonly FieldInfo ServerCleanOptionsField = Reflect.Field(typeof(PhotonNetwork), "ServerCleanOptions"); private static readonly MethodInfo RaiseEventInternalMethod = Reflect.Method(typeof(PhotonNetwork), "RaiseEventInternal"); internal static bool Available { get { if (RemoveFilterField != null && KeyByteSevenField != null && ServerCleanOptionsField != null) { return RaiseEventInternalMethod != null; } return false; } } internal static void ClearSceneCache() { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (!PunGate.Ready || !PhotonNetwork.IsMasterClient) { return; } if (!Available) { Log.Once("nocache", "Photon cache clearing is unavailable on this Photon build; skipping."); return; } PhotonView[] array; try { array = Object.FindObjectsOfType<PhotonView>(true); } catch (Exception ex) { Log.Verbose("Could not enumerate PhotonViews: " + ex.GetType().Name); return; } int num = 0; int num2 = 0; PhotonView[] array2 = array; foreach (PhotonView val in array2) { if ((Object)(object)val == (Object)null) { continue; } Scene scene = ((Component)val).gameObject.scene; if (((Scene)(ref scene)).buildIndex == -1) { num2++; continue; } if (ClearOne(val)) { num++; } ClearBufferedRpcs(val); } if (num > 0) { Log.Once("cachecleared", "Cleared " + num + " buffered instantiation(s) from the finished level, left " + num2 + " persistent object(s) alone."); } else { Log.Verbose("No buffered Photon instantiations needed clearing."); } } internal static bool ClearOne(PhotonView view) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)view == (Object)null || !Available) { return false; } if (!view.isRuntimeInstantiated || view.InstantiationId < 1) { return false; } try { object? value = RemoveFilterField.GetValue(null); Hashtable val = (Hashtable)((value is Hashtable) ? value : null); object value2 = KeyByteSevenField.GetValue(null); object? value3 = ServerCleanOptionsField.GetValue(null); RaiseEventOptions val2 = (RaiseEventOptions)((value3 is RaiseEventOptions) ? value3 : null); if (val == null || value2 == null || val2 == null) { return false; } val[value2] = view.InstantiationId; val2.CachingOption = (EventCaching)6; return RaiseInternal(202, val, val2); } catch (Exception ex) { Log.Verbose("Could not clear Photon cache for view " + view.ViewID + ": " + ex.GetType().Name); return false; } } internal static bool RaiseInternal(byte code, object data, RaiseEventOptions options) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (RaiseEventInternalMethod == null) { return false; } try { RaiseEventInternalMethod.Invoke(null, new object[4] { code, data, options, SendOptions.SendReliable }); return true; } catch (Exception ex) { Log.Verbose("Photon internal event " + code + " failed: " + ex.GetType().Name); return false; } } private static void ClearBufferedRpcs(PhotonView view) { if ((Object)(object)view == (Object)null || view.ViewID == 0) { return; } try { PhotonNetwork.RemoveBufferedRPCs(view.ViewID, (string)null, (int[])null); } catch (Exception ex) { Log.Verbose("Could not clear buffered RPCs for view " + view.ViewID + ": " + ex.GetType().Name); } } internal static void ClearStaleLevelUpdateRpc() { if (!PunGate.Ready || !PhotonNetwork.IsMasterClient || (Object)(object)RunManager.instance == (Object)null) { return; } try { object obj = Reflect.Field(typeof(RunManager), "runManagerPUN")?.GetValue(RunManager.instance); if (obj != null) { object? obj2 = Reflect.Field(obj.GetType(), "photonView")?.GetValue(obj); PhotonView val = (PhotonView)((obj2 is PhotonView) ? obj2 : null); if (val != null && !((Object)(object)val == (Object)null)) { PhotonNetwork.RemoveBufferedRPCs(val.ViewID, "UpdateLevelRPC", (int[])null); Log.Verbose("Removed the previous level's buffered UpdateLevelRPC."); } } } catch (Exception ex) { Log.Verbose("Could not remove the stale level update RPC: " + ex.GetType().Name); } } internal static void ClearStaleGenerationRpcs(LevelGenerator generator) { if (!PunGate.Ready || !PhotonNetwork.IsMasterClient || (Object)(object)generator == (Object)null) { return; } if (generator.Generated) { Log.Verbose("Generation already reported done; leaving its buffered RPCs alone."); return; } try { PhotonView photonView = generator.PhotonView; if (!((Object)(object)photonView == (Object)null) && photonView.ViewID != 0) { PhotonNetwork.RemoveBufferedRPCs(photonView.ViewID, (string)null, (int[])null); Log.Verbose("Removed the previous level's buffered generation RPCs."); } } catch (Exception ex) { Log.Verbose("Could not remove stale generation RPCs: " + ex.GetType().Name); } } } } namespace RepoLateJoin.Join { internal static class EnemyAwareness { private static readonly List<int> Scratch = new List<int>(); private static bool Wanted => ModConfig.On; private static List<int> PlayerViewIds() { Scratch.Clear(); if (GameDirector.instance?.PlayerList == null) { return Scratch; } foreach (PlayerAvatar player in GameDirector.instance.PlayerList) { if (!((Object)(object)player == (Object)null) && !((Object)(object)player.photonView == (Object)null) && player.photonView.ViewID != 0) { Scratch.Add(player.photonView.ViewID); } } return Scratch; } internal static void EnsureFor(EnemyOnScreen onScreen) { if (!Wanted || (Object)(object)onScreen == (Object)null) { return; } try { List<int> list = PlayerViewIds(); for (int i = 0; i < list.Count; i++) { onScreen.PlayerAdded(list[i]); } } catch (Exception ex) { Log.Verbose("EnemyOnScreen registration failed: " + ex.GetType().Name); } } internal static void EnsureFor(EnemyVision vision) { if (!Wanted || (Object)(object)vision == (Object)null) { return; } try { List<int> list = PlayerViewIds(); for (int i = 0; i < list.Count; i++) { vision.PlayerAdded(list[i]); } } catch (Exception ex) { Log.Verbose("EnemyVision registration failed: " + ex.GetType().Name); } } internal static void RegisterAllPlayers() { if (!Wanted) { return; } List<int> list = PlayerViewIds(); if (list.Count == 0) { return; } int num = 0; try { EnemyOnScreen[] array = Object.FindObjectsOfType<EnemyOnScreen>(true); foreach (EnemyOnScreen val in array) { if (!((Object)(object)val == (Object)null)) { for (int j = 0; j < list.Count; j++) { val.PlayerAdded(list[j]); } num++; } } EnemyVision[] array2 = Object.FindObjectsOfType<EnemyVision>(true); foreach (EnemyVision val2 in array2) { if (!((Object)(object)val2 == (Object)null)) { for (int k = 0; k < list.Count; k++) { val2.PlayerAdded(list[k]); } num++; } } } catch (Exception ex) { Log.Verbose("Could not sweep enemies for player registration: " + ex.GetType().Name); return; } if (num > 0) { Log.Once("enemyaware", "Registered " + list.Count + " player(s) with " + num + " enemy tracker(s) so enemies can see late joiners."); } else { Log.Verbose("No enemy trackers present yet; nothing to register."); } } internal static Type FindIteratorType(Type owner, string methodName) { if (owner == null) { return null; } string value = "<" + methodName + ">d__"; try { Type[] nestedTypes = owner.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); foreach (Type type in nestedTypes) { if (type.Name.StartsWith(value, StringComparison.Ordinal)) { return type; } } } catch (Exception ex) { Log.Verbose("Could not enumerate nested types of " + owner.Name + ": " + ex.GetType().Name); } Log.Once("noiter:" + owner.Name + "." + methodName, "Could not find the " + owner.Name + "." + methodName + " iterator on this game version; enemies may not track late joiners."); return null; } internal static T OwnerOf<T>(object stateMachine, FieldInfo thisField) where T : class { if (stateMachine == null || thisField == null) { return null; } try { return thisField.GetValue(stateMachine) as T; } catch { return null; } } } internal static class ExtractionSync { private static readonly FieldInfo RoundHaulGoalField = Reflect.Field(typeof(RoundDirector), "haulGoal"); private static readonly FieldInfo PointActiveField = Reflect.Field(typeof(RoundDirector), "extractionPointActive"); private static readonly FieldInfo PointCurrentField = Reflect.Field(typeof(RoundDirector), "extractionPointCurrent"); private static readonly FieldInfo PointListField = Reflect.Field(typeof(RoundDirector), "extractionPointList"); private static readonly FieldInfo PointStateField = Reflect.Field(typeof(ExtractionPoint), "currentState"); private static readonly FieldInfo PointViewField = Reflect.Field(typeof(ExtractionPoint), "photonView"); private static readonly FieldInfo PointHaulFetchedField = Reflect.Field(typeof(ExtractionPoint), "haulGoalFetched"); internal static int SendTo(Player target) { if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } if ((Object)(object)RoundDirector.instance == (Object)null) { return 0; } int num = 0 + SendHaulGoal(target) + SendActivation(target) + SendPointStates(target); if (num > 0) { Log.Once("extractsync", "Sent extraction point state to late joiners."); } return num; } private static int SendHaulGoal(Player target) { int num = Reflect.Get(RoundHaulGoalField, RoundDirector.instance, 0); if (num <= 0) { return 0; } PhotonView val = ViewOf(RoundDirector.instance); if ((Object)(object)val == (Object)null) { return 0; } try { val.RPC("StartRoundRPC", target, new object[1] { num }); return 1; } catch (Exception ex) { Log.Verbose("Could not send the haul goal: " + ex.GetType().Name); return 0; } } private static int SendActivation(Player target) { PhotonView val = ViewOf(RoundDirector.instance); if ((Object)(object)val == (Object)null) { return 0; } bool flag = Reflect.Get(PointActiveField, RoundDirector.instance, fallback: false); object? obj = PointCurrentField?.GetValue(RoundDirector.instance); ExtractionPoint val2 = (ExtractionPoint)((obj is ExtractionPoint) ? obj : null); try { if (flag && (Object)(object)val2 != (Object)null) { PhotonView val3 = ViewOf(val2); if ((Object)(object)val3 == (Object)null || val3.ViewID == 0) { return 0; } val.RPC("ExtractionPointActivateRPC", target, new object[1] { val3.ViewID }); Log.Verbose("Told the joiner extraction point " + val3.ViewID + " is active."); return 1; } val.RPC("ExtractionPointsUnlockRPC", target, Array.Empty<object>()); return 1; } catch (Exception ex) { Log.Verbose("Could not send the extraction activation state: " + ex.GetType().Name); return 0; } } private unsafe static int SendPointStates(Player target) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Invalid comparison between Unknown and I4 //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) int num = 0; foreach (ExtractionPoint item in Points()) { if ((Object)(object)item == (Object)null) { continue; } PhotonView val = ViewOf(item); if ((Object)(object)val == (Object)null || val.ViewID == 0) { continue; } try { if (item.haulGoal > 0 && Reflect.Get(PointHaulFetchedField, item, fallback: false)) { val.RPC("HaulGoalSetRPC", target, new object[1] { item.haulGoal }); num++; } if (PointStateField?.GetValue(item) is State val2 && (int)val2 != 1 && (int)val2 != 0) { val.RPC("StateSetRPC", target, new object[1] { val2 }); num++; Log.Verbose("Sent extraction point " + val.ViewID + " state " + ((object)(*(State*)(&val2))/*cast due to .constrained prefix*/).ToString() + "."); } } catch (Exception ex) { Log.Verbose("Could not send state for extraction point " + val.ViewID + ": " + ex.GetType().Name); } } return num; } private static IEnumerable<ExtractionPoint> Points() { if (PointListField?.GetValue(RoundDirector.instance) is List<GameObject> list) { foreach (GameObject item in list) { if (!((Object)(object)item == (Object)null)) { ExtractionPoint component = item.GetComponent<ExtractionPoint>(); if ((Object)(object)component != (Object)null) { yield return component; } } } yield break; } ExtractionPoint[] array; try { array = Object.FindObjectsOfType<ExtractionPoint>(true); } catch { yield break; } ExtractionPoint[] array2 = array; foreach (ExtractionPoint val in array2) { if ((Object)(object)val != (Object)null) { yield return val; } } } private static PhotonView ViewOf(RoundDirector director) { if ((Object)(object)director == (Object)null) { return null; } return ((Component)director).GetComponent<PhotonView>(); } private static PhotonView ViewOf(ExtractionPoint point) { if ((Object)(object)point == (Object)null) { return null; } object? obj = PointViewField?.GetValue(point); PhotonView val = (PhotonView)((obj is PhotonView) ? obj : null); if (val != null && (Object)(object)val != (Object)null) { return val; } return ((Component)point).GetComponent<PhotonView>(); } } internal static class JoinQueue { private const float AvatarWaitTimeout = 30f; private const int AnnounceAttempts = 8; private const float AnnounceInterval = 1.5f; private static readonly FieldInfo LevelAnimationCompletedField = Reflect.Field(typeof(PlayerAvatar), "levelAnimationCompleted"); private static readonly FieldInfo IsDisabledField = Reflect.Field(typeof(PlayerAvatar), "isDisabled"); private static readonly FieldInfo DeathHeadField = Reflect.Field(typeof(PlayerAvatar), "playerDeathHead"); private static readonly FieldInfo ColoursField = Reflect.Field(typeof(PlayerCosmetics), "colorsEquipped"); private static MonoBehaviour _runner; private static readonly HashSet<int> Active = new HashSet<int>(); private static readonly HashSet<int> Departed = new HashSet<int>(); internal static int PendingCount => Active.Count; internal static void Attach(MonoBehaviour runner) { _runner = runner; } internal static void Reset() { Active.Clear(); Departed.Clear(); ModuleConnectionSync.Clear(); } internal static void OnPlayerEntered(Player player) { if (!PunGate.Ready || player == null || (Object)(object)_runner == (Object)null) { return; } int actorNumber = player.ActorNumber; Player localPlayer = PhotonNetwork.LocalPlayer; if (actorNumber != ((localPlayer != null) ? new int?(localPlayer.ActorNumber) : ((int?)null))) { Departed.Remove(player.ActorNumber); bool flag = false; try { flag = SemiFunc.RunIsLevel() || SemiFunc.RunIsShop() || SemiFunc.RunIsLobby(); } catch { } if (!flag) { Log.Verbose("Player " + Describe(player) + " joined before the run started; no catch-up needed."); } else if (Active.Add(player.ActorNumber)) { Log.Info("Late joiner detected: " + Describe(player) + ". Starting catch-up."); _runner.StartCoroutine(CatchUp(player)); } } } internal static void OnPlayerLeft(Player player) { if (player != null) { Departed.Add(player.ActorNumber); Active.Remove(player.ActorNumber); } } private static IEnumerator CatchUp(Player player) { int actor = player.ActorNumber; string sceneAtStart = CurrentScene(); _runner.StartCoroutine(AnnounceLoadingFlag(player)); if (PhotonNetwork.IsMasterClient) { yield return (object)new WaitForSeconds(1.5f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before module replay"); yield break; } ModuleConnectionSync.ReplayTo(player); } float deadline = Time.unscaledTime + 30f; PlayerAvatar avatar = null; while (Time.unscaledTime < deadline) { if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted while waiting for the avatar"); yield break; } avatar = FindAvatar(player); if ((Object)(object)avatar != (Object)null) { break; } yield return (object)new WaitForSeconds(0.5f); } if ((Object)(object)avatar == (Object)null) { Finish(actor, "their avatar never appeared"); yield break; } EnemyAwareness.RegisterAllPlayers(); yield return null; if (!PhotonNetwork.IsMasterClient) { Finish(actor, "client-side catch-up complete"); yield break; } if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before stat sync"); yield break; } int num = StatsSync.SendTo(player); if (num > 0) { Log.Verbose("Sent " + num + " stat/total update(s) to " + Describe(player) + "."); } yield return (object)new WaitForSeconds(0.25f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before item name sync"); yield break; } int num2 = WorldStateSync.SendItemNamesTo(player); if (num2 > 0) { Log.Verbose("Sent " + num2 + " item identity update(s) to " + Describe(player) + "."); } yield return (object)new WaitForSeconds(0.25f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before battery sync"); yield break; } int num3 = WorldStateSync.SendBatteryStateTo(player); if (num3 > 0) { Log.Verbose("Sent " + num3 + " battery state(s) to " + Describe(player) + "."); } yield return (object)new WaitForSeconds(0.25f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before item state sync"); yield break; } int num4 = WorldStateSync.SendEquippedItemsTo(player); if (num4 > 0) { Log.Verbose("Sent " + num4 + " equipped-item state(s) to " + Describe(player) + "."); } yield return (object)new WaitForSeconds(0.25f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before value sync"); yield break; } int num5 = WorldStateSync.SendValuesTo(player); if (num5 > 0) { Log.Verbose("Sent " + num5 + " value update(s) to " + Describe(player) + "."); } yield return (object)new WaitForSeconds(0.25f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before extraction sync"); yield break; } int num6 = ExtractionSync.SendTo(player); if (num6 > 0) { Log.Verbose("Sent " + num6 + " extraction update(s) to " + Describe(player) + "."); } yield return (object)new WaitForSeconds(0.25f); if (Aborted(actor, sceneAtStart)) { Finish(actor, "aborted before level state sync"); yield break; } int num7 = LevelStateSync.SendTo(player); if (num7 > 0) { Log.Verbose("Sent " + num7 + " level state update(s) to " + Describe(player) + "."); } LevelSnapshot.SendTo(player); yield return (object)new WaitForSeconds(1f); if (!Aborted(actor, sceneAtStart)) { EnemyAwareness.RegisterAllPlayers(); } Finish(actor, "catch-up complete"); } private static IEnumerator AnnounceLoadingFlag(Player player) { int actor = player.ActorNumber; for (int attempt = 0; attempt < 8; attempt++) { yield return (object)new WaitForSeconds(1.5f); if (!ModConfig.On || Departed.Contains(actor)) { break; } PlayerAvatar val = LocalAvatar(); if (!((Object)(object)val == (Object)null) && !((Object)(object)val.photonView == (Object)null) && Reflect.Get(LevelAnimationCompletedField, val, fallback: false)) { if (attempt == 1) { ResendOwnVoice(val, player); } if (attempt == 5) { ResendOwnColours(val, player); } try { val.photonView.RPC("LoadingLevelAnimationCompletedRPC", player, Array.Empty<object>()); Log.Once("announce", "Announcing our loading-animation state to late joiners so their loading screen releases."); } catch (Exception ex) { Log.Verbose("Could not announce the loading flag: " + ex.GetType().Name); } } } } private static void ResendOwnVoice(PlayerAvatar local, Player player) { try { if (!Reflect.Get(IsDisabledField, local, fallback: false) && !((Object)(object)PlayerVoiceChat.instance == (Object)null)) { PhotonView component = ((Component)PlayerVoiceChat.instance).GetComponent<PhotonView>(); if ((Object)(object)component != (Object)null) { local.photonView.RPC("UpdateMyPlayerVoiceChat", player, new object[1] { component.ViewID }); } } } catch (Exception ex) { Log.Verbose("Could not resend our voice link: " + ex.GetType().Name); } } private static void ResendOwnColours(PlayerAvatar local, Player player) { SendColours(local.playerCosmetics, player); SendColours(Reflect.Get<PlayerDeathHead>(DeathHeadField, local)?.playerCosmetics, player); } private static void SendColours(PlayerCosmetics cosmetics, Player player) { try { int[] array = (((Object)(object)cosmetics != (Object)null) ? Reflect.Get<int[]>(ColoursField, cosmetics) : null); if (array != null && !Array.Exists(array, (int c) => c < 0)) { PhotonView component = ((Component)cosmetics).GetComponent<PhotonView>(); if (component != null) { component.RPC("SetupColorsRPC", player, new object[1] { array }); } } } catch (Exception ex) { Log.Verbose("Could not resend our colours: " + ex.GetType().Name); } } internal static void ReleaseOwnLoadingScreen(PlayerAvatar local) { if (!PunGate.Ready || PhotonNetwork.IsMasterClient || !PhotonNetwork.InRoom) { return; } bool flag; try { flag = SemiFunc.RunIsLevel() || SemiFunc.RunIsShop(); } catch { return; } if (!flag) { return; } if ((Object)(object)local == (Object)null) { local = LocalAvatar(); } if ((Object)(object)local == (Object)null || Reflect.Get(LevelAnimationCompletedField, local, fallback: false)) { return; } try { local.LoadingLevelAnimationCompleted(); Log.Once("selfrelease", "Released our own loading screen after joining a run in progress."); } catch (Exception ex) { Log.Verbose("Could not release our own loading screen: " + ex.GetType().Name); } } private static void Finish(int actor, string reason) { Active.Remove(actor); Log.Verbose("Catch-up for actor " + actor + " finished: " + reason + "."); } private static bool Aborted(int actor, string sceneAtStart) { if (!ModConfig.On) { return true; } if (Departed.Contains(actor)) { return true; } if (!PhotonNetwork.InRoom) { return true; } return CurrentScene() != sceneAtStart; } private static string CurrentScene() { try { RunManager instance = RunManager.instance; object obj; if (instance == null) { obj = null; } else { Level levelCurrent = instance.levelCurrent; obj = ((levelCurrent != null) ? ((Object)levelCurrent).name : null); } if (obj == null) { obj = string.Empty; } return (string)obj; } catch { return string.Empty; } } private static PlayerAvatar LocalAvatar() { try { return SemiFunc.PlayerAvatarLocal(); } catch { return null; } } private static PlayerAvatar FindAvatar(Player player) { try { return SemiFunc.PlayerAvatarGetFromPhotonPlayer(player); } catch { return null; } } private static string Describe(Player player) { if (player == null) { return "unknown"; } if (!string.IsNullOrEmpty(player.NickName)) { return player.NickName; } return "actor " + player.ActorNumber; } } internal static class LevelSnapshot { internal const byte Event = 172; private static readonly FieldInfo ExploredField = Reflect.Field(typeof(RoomVolume), "Explored"); internal static bool SendTo(Player target) { //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Expected O, but got Unknown //IL_00e0: Unknown result type (might be due to invalid IL or missing references) if (!PunGate.Ready) { return false; } if (target == null || !PhotonNetwork.IsMasterClient) { return false; } try { List<int> list = new List<int>(); RoomVolume[] array = Object.FindObjectsOfType<RoomVolume>(); foreach (RoomVolume val in array) { if (IsExplored(val)) { AddKey<RoomVolume>(list, val); } } List<int> list2 = new List<int>(); ValuablePropSwitch[] array2 = Object.FindObjectsOfType<ValuablePropSwitch>(true); foreach (ValuablePropSwitch val2 in array2) { if ((Object)(object)val2.PropParent != (Object)null && AddKey<ValuablePropSwitch>(list2, val2)) { list2.Add(val2.PropParent.activeSelf ? 1 : 0); } } RaiseEventOptions val3 = new RaiseEventOptions(); val3.TargetActors = new int[1] { target.ActorNumber }; RaiseEventOptions val4 = val3; return PhotonNetwork.RaiseEvent((byte)172, (object)new object[2] { list.ToArray(), list2.ToArray() }, val4, SendOptions.SendReliable); } catch (Exception ex) { Log.Verbose("Could not send the level snapshot: " + ex.GetType().Name); return false; } } private static bool IsExplored(RoomVolume room) { return Reflect.Get(ExploredField, room, fallback: false); } private static bool AddKey<T>(List<int> into, T item) where T : Component { PhotonView componentInParent = ((Component)item).GetComponentInParent<PhotonView>(true); if ((Object)(object)componentInParent == (Object)null || componentInParent.ViewID < 1) { return false; } int num = Array.IndexOf(((Component)componentInParent).GetComponentsInChildren<T>(true), item); if (num < 0) { return false; } into.Add(componentInParent.ViewID); into.Add(num); return true; } internal static void Apply(EventData photonEvent) { if (!ModConfig.On || PhotonNetwork.IsMasterClient || !(photonEvent.CustomData is object[] array) || array.Length < 2) { return; } try { int num = 0; int num2 = 0; if (array[0] is int[] array2) { for (int i = 0; i + 1 < array2.Length; i += 2) { RoomVolume val = Find<RoomVolume>(array2[i], array2[i + 1]); if (val != null && val.SetExplored()) { num++; } } } if (array[1] is int[] array3) { for (int j = 0; j + 2 < array3.Length; j += 3) { ValuablePropSwitch val2 = Find<ValuablePropSwitch>(array3[j], array3[j + 1]); if (val2 != null) { bool flag = array3[j + 2] == 1; val2.PropParent.SetActive(flag); if ((Object)(object)val2.ValuableParent != (Object)null) { val2.ValuableParent.SetActive(!flag); } num2++; } } } Log.Info("Applied the host's level snapshot: " + num + " explored room(s), " + num2 + " prop switch(es)."); } catch (Exception ex) { Log.Verbose("Could not apply the level snapshot: " + ex.GetType().Name); } } private static T Find<T>(int viewId, int index) where T : Component { PhotonView val = PhotonView.Find(viewId); if ((Object)(object)val == (Object)null) { return default(T); } T[] componentsInChildren = ((Component)val).GetComponentsInChildren<T>(true); if (index < 0 || index >= componentsInChildren.Length) { return default(T); } return componentsInChildren[index]; } } internal static class LevelStateSync { private static readonly FieldInfo EnemySpawnedField = Reflect.Field(typeof(EnemyParent), "Spawned"); private static readonly FieldInfo DiscoveredField = Reflect.Field(typeof(ValuableObject), "discovered"); private static readonly FieldInfo HingeBrokenField = Reflect.Field(typeof(PhysGrabHinge), "broken"); private static readonly FieldInfo HingeDeadField = Reflect.Field(typeof(PhysGrabHinge), "dead"); private static readonly FieldInfo CosmeticHealthField = Reflect.Field(typeof(CosmeticWorldObject), "healthAmountTarget"); private static readonly FieldInfo DeadSetField = Reflect.Field(typeof(PlayerAvatar), "deadSet"); private static readonly FieldInfo SteamIdField = Reflect.Field(typeof(PlayerAvatar), "steamID"); private static readonly FieldInfo HealthField = Reflect.Field(typeof(PlayerHealth), "health"); private static readonly FieldInfo MaxHealthField = Reflect.Field(typeof(PlayerHealth), "maxHealth"); private static readonly FieldInfo StandBrokenField = Reflect.Field(typeof(UpgradeStand), "isBroken"); private static readonly FieldInfo KeycardStateField = Reflect.Field(typeof(ShopKeycardDoor), "currentState"); private static readonly FieldInfo ChargingField = Reflect.Field(typeof(ChargingStation), "isCharging"); private static readonly FieldInfo IsMeleeField = Reflect.Field(typeof(PhysGrabObject), "isMelee"); private static readonly FieldInfo MineStateField = Reflect.Field(typeof(ItemMine), "state"); private static readonly FieldInfo TvBrokenField = Reflect.Field(typeof(FlatScreenTV), "broken"); private static readonly FieldInfo VehicleStageField = Reflect.Field(typeof(ItemVehicle), "currentDestructionStateIndex"); private static readonly Dictionary<int, int> DeadChildren = new Dictionary<int, int>(); internal static void Clear() { DeadChildren.Clear(); } internal static void RememberDestroyed(PhotonView view) { if (!((Object)(object)view == (Object)null) && view.ViewID >= 1 && view.ViewID != view.InstantiationId) { DeadChildren[view.ViewID] = view.Prefix; } } internal static int SendTo(Player target) { if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } int num = 0 + SendPropSwitches(target) + SendDeadChildren(target) + SendHinges(target) + SendEnemies(target) + SendDiscovered(target) + SendCosmeticHealth(target) + SendDeathHeads(target) + SendPlayers(target) + SendHaulList(target) + SendMisc(target); if (num > 0) { Log.Once("levelsync", "Sent props, enemies, doors and map state to late joiners."); } return num; } private static int SendPropSwitches(Player target) { if ((Object)(object)ValuableDirector.instance == (Object)null) { return 0; } return Rpc((Component)(object)ValuableDirector.instance, "VolumesAndSwitchSetupRPC", target) ? 1 : 0; } private static int SendDeadChildren(Player target) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Expected O, but got Unknown int num = 0; foreach (KeyValuePair<int, int> deadChild in DeadChildren) { Hashtable val = new Hashtable { [(byte)0] = deadChild.Key, [(byte)2] = PhotonNetwork.ServerTimestamp, [(byte)3] = "DestroyPhysGrabObjectRPC" }; if (deadChild.Value > 0) { val[(byte)1] = (short)deadChild.Value; } RaiseEventOptions val2 = new RaiseEventOptions(); val2.TargetActors = new int[1] { target.ActorNumber }; if (PhotonCacheCleaner.RaiseInternal(200, val, val2)) { num++; } } return num; } private static int SendHinges(Player target) { int num = 0; PhysGrabHinge[] array = Object.FindObjectsOfType<PhysGrabHinge>(); foreach (PhysGrabHinge val in array) { if (Reflect.Get(HingeBrokenField, val, fallback: false) && Rpc((Component)(object)val, "HingeBreakRPC", target)) { num++; } if (Reflect.Get(HingeDeadField, val, fallback: false) && Rpc((Component)(object)val, "DestroyHingeRPC", target)) { num++; } } return num; } private static int SendEnemies(Player target) { int num = 0; EnemyParent[] array = Object.FindObjectsOfType<EnemyParent>(true); foreach (EnemyParent val in array) { bool flag = Reflect.Get(EnemySpawnedField, val, fallback: false); if (Rpc((Component)(object)val, flag ? "SpawnRPC" : "DespawnRPC", target)) { num++; } } return num; } private static int SendDiscovered(Player target) { int num = 0; ValuableObject[] array = Object.FindObjectsOfType<ValuableObject>(); foreach (ValuableObject val in array) { if (Reflect.Get(DiscoveredField, val, fallback: false) && Rpc((Component)(object)val, "DiscoverRPC", target)) { num++; } } return num; } private static int SendCosmeticHealth(Player target) { int num = 0; CosmeticWorldObject[] array = Object.FindObjectsOfType<CosmeticWorldObject>(); foreach (CosmeticWorldObject val in array) { if (Rpc((Component)(object)val, "HealthUpdateRPC", target, Reflect.Get(CosmeticHealthField, val, 1f))) { num++; } } return num; } private static int SendDeathHeads(Player target) { int num = 0; PlayerDeathHead[] array = Object.FindObjectsOfType<PlayerDeathHead>(true); foreach (PlayerDeathHead val in array) { string text = (((Object)(object)val.playerAvatar != (Object)null) ? Reflect.Get<string>(SteamIdField, val.playerAvatar) : null); if (!string.IsNullOrEmpty(text) && Rpc((Component)(object)val, "SetupRPC", target, text)) { num++; } } return num; } private static int SendPlayers(Player target) { int num = 0; PlayerAvatar[] array = Object.FindObjectsOfType<PlayerAvatar>(true); foreach (PlayerAvatar val in array) { if ((Object)(object)val.photonView == (Object)null || val.photonView.Owner == target) { continue; } if (Reflect.Get(DeadSetField, val, fallback: false)) { if (Rpc((Component)(object)val, "PlayerDeathRPC", target, -1)) { num++; } } else if ((Object)(object)val.playerHealth != (Object)null && Rpc((Component)(object)val.playerHealth, "UpdateHealthRPC", target, Reflect.Get(HealthField, val.playerHealth, 100), Reflect.Get(MaxHealthField, val.playerHealth, 100), false, false)) { num++; } } return num; } private static int SendHaulList(Player target) { int num = 0; if ((Object)(object)RoundDirector.instance == (Object)null) { return 0; } foreach (GameObject dollarHaul in RoundDirector.instance.dollarHaulList) { if ((Object)(object)dollarHaul != (Object)null && Rpc((Component)(object)dollarHaul.transform, "AddToDollarHaulListRPC", target)) { num++; } } return num; } private static int SendMisc(Player target) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected I4, but got Unknown //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Invalid comparison between Unknown and I4 int num = 0; UpgradeStand[] array = Object.FindObjectsOfType<UpgradeStand>(); foreach (UpgradeStand val in array) { if (Reflect.Get(StandBrokenField, val, fallback: false) && Rpc((Component)(object)val, "SetBrokenRPC", target)) { num++; } } ShopKeycardDoor[] array2 = Object.FindObjectsOfType<ShopKeycardDoor>(); foreach (ShopKeycardDoor val2 in array2) { int num2 = (int)Reflect.Get<State>(KeycardStateField, val2, (State)0); if (num2 != 0 && Rpc((Component)(object)val2, "StateSetRPC", target, num2)) { num++; } } ChargingStation[] array3 = Object.FindObjectsOfType<ChargingStation>(); foreach (ChargingStation val3 in array3) { if (Reflect.Get(ChargingField, val3, fallback: false) && Rpc((Component)(object)val3, "StartChargeRPC", target)) { num++; } } ItemMelee[] array4 = Object.FindObjectsOfType<ItemMelee>(); foreach (ItemMelee val4 in array4) { PhysGrabObject component = ((Component)val4).GetComponent<PhysGrabObject>(); if ((Object)(object)component != (Object)null && !Reflect.Get(IsMeleeField, component, fallback: true) && Rpc((Component)(object)val4, "MeleeBreakRPC", target)) { num++; } } ItemMine[] array5 = Object.FindObjectsOfType<ItemMine>(); foreach (ItemMine val5 in array5) { if ((int)Reflect.Get<States>(MineStateField, val5, (States)0) == 2 && Rpc((Component)(object)val5, "StateSetRPC", target, 2)) { num++; } } FlatScreenTV[] array6 = Object.FindObjectsOfType<FlatScreenTV>(); foreach (FlatScreenTV val6 in array6) { if (Rpc((Component)(object)val6, "BrokenOrNotRPC", target, Reflect.Get(TvBrokenField, val6, fallback: true))) { num++; } } ItemVehicle[] array7 = Object.FindObjectsOfType<ItemVehicle>(); foreach (ItemVehicle val7 in array7) { int num3 = Reflect.Get(VehicleStageField, val7, -1); if (num3 >= 0 && Rpc((Component)(object)val7, "ChangeDestructionStateRPC", target, num3)) { num++; } } return num; } private static bool Rpc(Component owner, string method, Player target, params object[] args) { PhotonView val = (((Object)(object)owner != (Object)null) ? owner.GetComponent<PhotonView>() : null); if ((Object)(object)val == (Object)null || val.ViewID < 1) { return false; } try { val.RPC(method, target, args); return true; } catch (Exception ex) { Log.Verbose("Could not replay " + method + " on view " + val.ViewID + ": " + ex.GetType().Name); return false; } } } internal static class ModuleConnectionSync { private sealed class ConnectionState { internal PhotonView View; internal bool Top; internal bool Bottom; internal bool Right; internal bool Left; internal bool First; } private const byte RepairRequestEvent = 171; private const float FirstRequestDelay = 8f; private const float RequestInterval = 10f; private const int MaxRequests = 5; private static readonly FieldInfo PhotonViewField = Reflect.Field(typeof(Module), "photonView"); private static readonly FieldInfo SetupDoneField = Reflect.Field(typeof(Module), "SetupDone"); private static readonly Dictionary<int, ConnectionState> Known = new Dictionary<int, ConnectionState>(); private static readonly HashSet<int> ServedActors = new HashSet<int>(); private static float _unsetupSince; private static float _nextRequest; private static int _requestsSent; private static bool _listening; private static bool Wanted => PunGate.Ready; internal static int KnownCount => Known.Count; internal static void Clear() { if (Known.Count > 0) { Log.Verbose("Cleared " + Known.Count + " remembered module connection state(s)."); } Known.Clear(); ServedActors.Clear(); _unsetupSince = 0f; _nextRequest = 0f; _requestsSent = 0; } internal static void StartListening() { if (_listening) { return; } try { PhotonNetwork.NetworkingClient.EventReceived += OnEvent; _listening = true; Log.Verbose("Listening for module repair requests on event " + (byte)171 + "."); } catch (Exception ex) { Log.Warn("Could not subscribe to Photon events; module repair requests are unavailable: " + ex.GetType().Name); } } internal static void StopListening() { if (_listening) { try { PhotonNetwork.NetworkingClient.EventReceived -= OnEvent; } catch { } _listening = false; } } private static void OnEvent(EventData photonEvent) { if (photonEvent.Code == 172) { LevelSnapshot.Apply(photonEvent); } else { if (photonEvent.Code != 171 || !Wanted || !PhotonNetwork.IsMasterClient) { return; } try { if (photonEvent.CustomData is int[] array && array.Length != 0) { Player value = null; if (PhotonNetwork.CurrentRoom != null) { PhotonNetwork.CurrentRoom.Players?.TryGetValue(photonEvent.Sender, out value); } if (value != null) { int num = ReplaySubsetTo(value, array); Log.Info("Repaired " + num + " module connection(s) on request from " + Describe(value) + "."); } } } catch (Exception ex) { Log.Verbose("Could not handle a module repair request: " + ex.GetType().Name); } } } internal static void Remember(Module module, bool top, bool bottom, bool right, bool left, bool first) { if (Wanted && !((Object)(object)module == (Object)null) && SemiFunc.IsMultiplayer() && PhotonNetwork.IsMasterClient) { PhotonView val = ViewOf(module); if (!((Object)(object)val == (Object)null) && val.ViewID != 0) { Known[val.ViewID] = new ConnectionState { View = val, Top = top, Bottom = bottom, Right = right, Left = left, First = first }; Log.Once("modremember", "Recording module connection states so late joiners can be caught up."); } } } internal static int ReplayTo(Player target) { if (!Wanted || target == null || Known.Count == 0) { return 0; } if (!PhotonNetwork.IsMasterClient) { return 0; } if (!ServedActors.Add(target.ActorNumber)) { Log.Verbose("Already sent module connections to " + Describe(target) + "; not resending."); return 0; } int num = Send(target, Known.Keys); if (num > 0) { Log.Info("Sent " + num + " module connection state(s) to late joiner " + Describe(target) + "."); } return num; } private static int ReplaySubsetTo(Player target, int[] viewIds) { ServedActors.Add(target.ActorNumber); return Send(target, viewIds); } private static int Send(Player target, IEnumerable<int> viewIds) { int num = 0; int num2 = 0; foreach (int viewId in viewIds) { if (!Known.TryGetValue(viewId, out var value) || (Object)(object)value?.View == (Object)null) { num2++; continue; } try { value.View.RPC("ModuleConnectionSetRPC", target, new object[5] { value.Top, value.Bottom, value.Right, value.Left, value.First }); num++; } catch (Exception ex) { Log.Verbose("Could not send module connection " + viewId + ": " + ex.GetType().Name); } } if (num2 > 0) { Log.Verbose(num2 + " requested module view(s) are unknown to us and were skipped."); } return num; } internal static void TickClientRepair() { //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Expected O, but got Unknown //IL_00e0: Unknown result type (might be due to invalid IL or missing references) if (!Wanted || PhotonNetwork.IsMasterClient || !PhotonNetwork.InRoom) { return; } bool flag; try { flag = SemiFunc.RunIsLevel() || SemiFunc.RunIsShop(); } catch { return; } if (!flag || _requestsSent >= 5) { _unsetupSince = 0f; return; } List<int> list = CollectUnsetupViewIds(); if (list.Count == 0) { if (_unsetupSince != 0f) { Log.Verbose("All module connection states are present; repair not needed."); } _unsetupSince = 0f; } else if (_unsetupSince == 0f) { _unsetupSince = Time.unscaledTime; } else if (!(Time.unscaledTime - _unsetupSince < 8f) && !(Time.unscaledTime < _nextRequest)) { _nextRequest = Time.unscaledTime + 10f; _requestsSent++; try { RaiseEventOptions val = new RaiseEventOptions { Receivers = (ReceiverGroup)2 }; PhotonNetwork.RaiseEvent((byte)171, (object)list.ToArray(), val, SendOptions.SendReliable); Log.Info("Asked the host to repair " + list.Count + " module connection state(s) we never received (attempt " + _requestsSent + " of " + 5 + ")."); } catch (Exception ex) { Log.Verbose("Could not send a module repair request: " + ex.GetType().Name); } } } private static List<int> CollectUnsetupViewIds() { List<int> list = new List<int>(); Module[] array; try { array = Object.FindObjectsOfType<Module>(); } catch { return list; } Module[] array2 = array; foreach (Module val in array2) { if (!((Object)(object)val == (Object)null) && !Reflect.Get(SetupDoneField, val, fallback: true) && !((Object)(object)((Component)val).GetComponent<StartRoom>() != (Object)null)) { PhotonView val2 = ViewOf(val); if (!((Object)(object)val2 == (Object)null) && val2.ViewID != 0) { list.Add(val2.ViewID); } } } return list; } private static PhotonView ViewOf(Module module) { object? obj = PhotonViewField?.GetValue(module); PhotonView val = (PhotonView)((obj is PhotonView) ? obj : null); if (val != null && (Object)(object)val != (Object)null) { return val; } return ((Component)module).GetComponent<PhotonView>(); } private static string Describe(Player player) { if (player == null) { return "unknown"; } if (!string.IsNullOrEmpty(player.NickName)) { return player.NickName; } return "actor " + player.ActorNumber; } } internal static class StatsSync { private static readonly FieldInfo TotalHaulField = Reflect.Field(typeof(RoundDirector), "totalHaul"); internal static int SendTo(Player target) { if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } int num = 0 + ResyncStatDictionaries() + SendHaulTotal(target) + SendShoppingCost(target); if (num > 0) { Log.Once("statsync", "Re-sent run statistics and totals for late joiners."); } return num; } private static int ResyncStatDictionaries() { if ((Object)(object)PunManager.instance == (Object)null) { return 0; } try { PunManager.instance.SyncAllDictionaries(); Log.Verbose("Re-ran the full stat dictionary sync for the new player."); return 1; } catch (Exception ex) { Log.Verbose("Could not re-run the stat dictionary sync: " + ex.GetType().Name); return 0; } } private static int SendHaulTotal(Player target) { if ((Object)(object)PunManager.instance == (Object)null || (Object)(object)RoundDirector.instance == (Object)null) { return 0; } PhotonView component = ((Component)PunManager.instance).GetComponent<PhotonView>(); if ((Object)(object)component == (Object)null || component.ViewID == 0) { return 0; } int num = Reflect.Get(TotalHaulField, RoundDirector.instance, 0); try { component.RPC("SyncHaul", target, new object[1] { num }); Log.Verbose("Sent the current haul total (" + num + ") to the new player."); return 1; } catch (Exception ex) { Log.Verbose("Could not send the haul total: " + ex.GetType().Name); return 0; } } private static int SendShoppingCost(Player target) { if ((Object)(object)PunManager.instance == (Object)null || (Object)(object)ShopManager.instance == (Object)null) { return 0; } PhotonView component = ((Component)PunManager.instance).GetComponent<PhotonView>(); if ((Object)(object)component == (Object)null || component.ViewID == 0) { return 0; } try { component.RPC("UpdateShoppingCostRPC", target, new object[1] { ShopManager.instance.totalCost }); Log.Verbose("Sent the current cart total (" + ShopManager.instance.totalCost + ")."); return 1; } catch (Exception ex) { Log.Verbose("Could not send the cart total: " + ex.GetType().Name); return 0; } } } internal static class WorldStateSync { private static readonly FieldInfo ItemValueField = Reflect.Field(typeof(ItemAttributes), "value"); private static readonly FieldInfo ValuableCurrentField = Reflect.Field(typeof(ValuableObject), "dollarValueCurrent"); private static readonly FieldInfo ValuableSetField = Reflect.Field(typeof(ValuableObject), "dollarValueSet"); private static readonly FieldInfo InstanceNameField = Reflect.Field(typeof(ItemAttributes), "instanceName"); private static readonly FieldInfo ItemStateField = Reflect.Field(typeof(ItemEquippable), "currentState"); private static readonly FieldInfo ItemSpotIndexField = Reflect.Field(typeof(ItemEquippable), "inventorySpotIndex"); private static readonly FieldInfo ItemOwnerField = Reflect.Field(typeof(ItemEquippable), "ownerPlayerId"); private static readonly FieldInfo BatteryLifeIntField = Reflect.Field(typeof(ItemBattery), "batteryLifeInt"); internal static int SendValuesTo(Player target) { if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } int num = 0; try { ItemAttributes[] array = Object.FindObjectsOfType<ItemAttributes>(true); foreach (ItemAttributes val in array) { if ((Object)(object)val == (Object)null) { continue; } PhotonView component = ((Component)val).GetComponent<PhotonView>(); if (!((Object)(object)component == (Object)null) && component.ViewID != 0) { int num2 = Reflect.Get(ItemValueField, val, 0); if (num2 > 0) { component.RPC("GetValueRPC", target, new object[1] { num2 }); num++; } } } } catch (Exception ex) { Log.Verbose("Item value sync failed: " + ex.GetType().Name); } try { ValuableObject[] array2 = Object.FindObjectsOfType<ValuableObject>(true); foreach (ValuableObject val2 in array2) { if ((Object)(object)val2 == (Object)null || !Reflect.Get(ValuableSetField, val2, fallback: false)) { continue; } PhotonView component2 = ((Component)val2).GetComponent<PhotonView>(); if (!((Object)(object)component2 == (Object)null) && component2.ViewID != 0) { float num3 = Reflect.Get(ValuableCurrentField, val2, 0f); if (!(num3 <= 0f)) { component2.RPC("DollarValueSetRPC", target, new object[1] { num3 }); num++; } } } } catch (Exception ex2) { Log.Verbose("Valuable value sync failed: " + ex2.GetType().Name); } if (num > 0) { Log.Once("valuesync", "Sent item and valuable values to late joiners."); } return num; } internal static int SendItemNamesTo(Player target) { if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } if ((Object)(object)PunManager.instance == (Object)null) { return 0; } PhotonView component = ((Component)PunManager.instance).GetComponent<PhotonView>(); if ((Object)(object)component == (Object)null || component.ViewID == 0) { return 0; } ItemAttributes[] array; try { array = Object.FindObjectsOfType<ItemAttributes>(true); } catch (Exception ex) { Log.Verbose("Could not enumerate items for name sync: " + ex.GetType().Name); return 0; } int num = 0; ItemAttributes[] array2 = array; foreach (ItemAttributes val in array2) { if ((Object)(object)val == (Object)null) { continue; } string text = Reflect.Get<string>(InstanceNameField, val); if (string.IsNullOrEmpty(text)) { continue; } PhotonView component2 = ((Component)val).GetComponent<PhotonView>(); if (!((Object)(object)component2 == (Object)null) && component2.ViewID != 0) { try { component.RPC("SetItemNameRPC", target, new object[2] { text, component2.ViewID }); num++; } catch (Exception ex2) { Log.Verbose("Could not send the item name for " + ((Object)val).name + ": " + ex2.GetType().Name); } } } if (num > 0) { Log.Once("itemnames", "Sent item identities, battery levels and inventory placement to late joiners."); } return num; } internal static int SendEquippedItemsTo(Player target) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Expected I4, but got Unknown if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } ItemEquippable[] array; try { array = Object.FindObjectsOfType<ItemEquippable>(true); } catch (Exception ex) { Log.Verbose("Could not enumerate equippable items: " + ex.GetType().Name); return 0; } int num = 0; ItemEquippable[] array2 = array; foreach (ItemEquippable val in array2) { if ((Object)(object)val == (Object)null || !(ItemStateField?.GetValue(val) is ItemState val2) || (int)val2 == 0) { continue; } int num2 = Reflect.Get(ItemOwnerField, val, -1); if (num2 == -1) { continue; } int num3 = Reflect.Get(ItemSpotIndexField, val, -1); PhotonView component = ((Component)val).GetComponent<PhotonView>(); if (!((Object)(object)component == (Object)null) && component.ViewID != 0) { try { component.RPC("RPC_UpdateItemState", target, new object[3] { (int)val2, num3, num2 }); num++; } catch (Exception ex2) { Log.Verbose("Could not send item state for " + ((Object)val).name + ": " + ex2.GetType().Name); } } } if (num > 0) { Log.Once("itemstate", "Sent equipped-item state to late joiners."); } return num; } internal static int SendBatteryStateTo(Player target) { if (!PunGate.Ready) { return 0; } if (target == null || !PhotonNetwork.IsMasterClient) { return 0; } int num = 0; ItemBattery[] array; try { array = Object.FindObjectsOfType<ItemBattery>(true); } catch (Exception ex) { Log.Verbose("Could not enumerate batteries: " + ex.GetType().Name); return 0; } ItemBattery[] array2 = array; foreach (ItemBattery val in array2) { if ((Object)(object)val == (Object)null) { continue; } PhotonView component = ((Component)val).GetComponent<PhotonView>(); if (!((Object)(object)component == (Object)null) && component.ViewID != 0 && !((Object)(object)((Component)component).GetComponent<ItemBattery>() != (Object)(object)val)) { try { int num2 = CurrentBarLevel(val); bool batteryActive = val.batteryActive; component.RPC("BatteryToggleRPC", target, new object[1] { false }); component.RPC("BatteryFullPercentChangeRPC", target, new object[2] { num2, false }); component.RPC("BatteryToggleRPC", target, new object[1] { batteryActive }); num++; } catch (Exception ex2) { Log.Verbose("Battery sync failed for " + ((Object)val).name + ": " + ex2.GetType().Name); } } } if (num > 0) { Log.Once("batterysync", "Sent battery charge states to late joiners."); } return num; } private static int CurrentBarLevel(ItemBattery battery) { int num = Mathf.Max(1, battery.batteryBars); int num2 = Reflect.Get(BatteryLifeIntField, battery, 0); if (num2 < 0 || num2 > num || (num2 == 0 && battery.batteryLife > 0f)) { num2 = Mathf.RoundToInt(battery.batteryLife / (100f / (float)num)); } return Mathf.Clamp(num2, 0, num); } } } namespace RepoLateJoin.Core { internal static class Log { private static ManualLogSource _source; private static readonly HashSet<string> Seen = new HashSet<string>(); internal static void Attach(ManualLogSource source) { _source = source; } internal static void Info(string message) { ManualLogSource source = _source; if (source != null) { source.LogInfo((object)message); } } internal static void Warn(string message) { ManualLogSource source = _source; if (source != null) { source.LogWarning((object)message); } } internal static void Error(string message) { ManualLogSource source = _source; if (source != null) { source.LogError((object)message); } } internal static void Verbose(string message) { ManualLogSource source = _source; if (source != null) { source.LogDebug((object)message); } } internal static void Once(string key, string message) { if (Seen.Add(key)) { Info(message); } else { Verbose(message); } } internal static void Reset() { Seen.Clear(); } } internal static class ModConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> JoinInLobby; internal static ConfigEntry<bool> JoinInShop; internal static ConfigEntry<bool> JoinInLevel; internal static ConfigEntry<bool> ShowStatus; private const string Section = "Late Join / 중간 참여"; internal static bool On { get { if (Enabled != null) { return Enabled.Value; } return false; } } internal static void Bind(ConfigFile cfg) { Enabled = cfg.Bind<bool>("Late Join / 중간 참여", "Enabled", true, "Late join on/off. / 중간 참여 켜기/끄기."); JoinInLobby = cfg.Bind<bool>("Late Join / 중간 참여", "Truck", true, "Allow joining in the truck. / 트럭에서 참여 허용."); JoinInShop = cfg.Bind<bool>("Late Join / 중간 참여", "Shop", true, "Allow joining in the shop. / 상점에서 참여 허용."); JoinInLevel = cfg.Bind<bool>("Late Join / 중간 참여", "Level", true, "Allow joining during a level. / 레벨 진행 중 참여 허용."); ShowStatus = cfg.Bind<bool>("Late Join / 중간 참여", "ShowStatus", false, "Show room name and late join status top-left. / 좌측 상단에 방 이름과 중간 참여 상태 표시."); } } internal static class PunGate { private static bool _ready; internal static bool Ready { get { if (_ready) { return ModConfig.On; } return false; } } internal static bool PhotonInitialised => _ready; internal static void Mark() { if (!_ready) { _ready = true; Log.Verbose("Photon is initialised by the game; network-dependent work is now allowed."); } } } internal static class Reflect { private static readonly Dictionary<string, FieldInfo> FieldCache = new Dictionary<string, FieldInfo>(); private static readonly Dictionary<string, MethodInfo> MethodCache = new Dictionary<string, MethodInfo>(); internal static FieldInfo Field(Type type, string name) { if (type == null || string.IsNullOrEmpty(name)) { return null; } string text = type.FullName + "::" + name; if (FieldCache.TryGetValue(text, out var value)) { return value; } FieldInfo fieldInfo = null; try { fieldInfo = AccessTools.Field(type, name); } catch (Exception ex) { Log.Verbose("Field lookup failed for " + text + ": " + ex.GetType().Name); } FieldCache[text] = fieldInfo; if (fieldInfo == null) { Log.Once("nofield:" + text, "Field " + text + " is missing on this game version."); } return fieldInfo; } internal static MethodInfo Method(Type type, string name, Type[] parameters = null) { if (type == null || string.IsNullOrEmpty(name)) { return null; } string text = type.FullName + "::" + name + "/" + ((parameters != null) ? parameters.Length : (-1)); if (MethodCache.TryGetValue(text, out var value)) { return value; } MethodInfo methodInfo = null; try { methodInfo = AccessTools.Method(type, name, parameters, (Type[])null); } catch (Exception ex) { Log.Verbose("Method lookup failed for " + text + ": " + ex.GetType().Name); } MethodCache[text] = methodInfo; if (methodInfo == null) { Log.Once("nomethod:" + text, "Method " + text + " is missing on this game version."); } return methodInfo; } internal static Type TypeByName(string name) { try { return AccessTools.TypeByName(name); } catch { return null; } } internal static T Get<T>(FieldInfo field, object instance, T fallback = default(T)) { if (field == null) { return fallback; } try { return (field.GetValue(instance) is T val) ? val : fallback; } catch { return fallback; } } internal static bool Set(FieldInfo field, object instance, object value) { if (field == null) { return false; } try { field.SetValue(instance, value); return true; } catch (Exception ex) { Log.Verbose("Could not write " + field.Name + ": " + ex.GetType().Name); return false; } } internal static bool Invoke(MethodInfo method, object instance, params object[] args) { if (method == null) { return false; } try { method.Invoke(instance, args); return true; } catch (Exception ex) { Log.Verbose("Could not invoke " + method.Name + ": " + ex.GetType().Name); return false; } } } } namespace RepoLateJoin.Client { internal static class ClientGuards { private const float SettleSeconds = 20f; private static float _settleUntil; private static bool _armed; private static readonly HashSet<string> Reported = new HashSet<string>(); private static readonly (string Type, string Method)[] Targets = new(string, string)[11] { ("EnergyUI", "Update"), ("PlayerAvatarVisuals", "Update"), ("PlayerExpression", "Update"), ("PlayerAvatarTalkAnimation", "Update"), ("PlayerAvatarOverchargeVisuals", "Update"), ("PlayerAvatarGrabDisableVisuals", "Update"), ("PlayerAvatarLeftArm", "Update"), ("PlayerLocalCamera", "GetOverrideTransform"), ("PhysGrabber", "Update"), ("ItemEquippable", "RPC_UpdateItemState"), ("LoadingUI", "LevelAnimationStart") }; internal static bool Settling { get { if (ModConfig.On && _armed) { return Time.unscaledTime < _settleUntil; } return false; } } internal static void ArmForLateJoin() { _armed = true; _settleUntil = Time.unscaledTime + 20f; Log.Once("guardsarmed", "Late-join client guards armed for " + 20f + "s while our client finishes wiring up."); } internal static void Disarm() { _armed = false; _settleUntil = 0f; Reported.Clear(); } internal static void EvaluateLocalState(PlayerAvatar localAvatar) { if (!PunGate.Ready || PhotonNetwork.IsMasterClient || !PhotonNetwork.InRoom || (Object)(object)localAvatar == (Object)null || _armed) { return; } try { if ((Object)(object)LevelGenerator.Instance == (Object)null || !LevelGenerator.Instance.Generated || (!SemiFunc.RunIsLevel() && !SemiFunc.RunIsShop())) { return; } } catch { return; } ArmForLateJoin(); } internal static int Install(Harmony harmony) { //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Expected O, but got Unknown MethodInfo methodInfo = Reflect.Method(typeof(ClientGuards), "Swallow"); if (methodInfo == null) { Log.Warn("Could not resolve the guard finalizer; client guards will not be installed."); return 0; } int num = 0; (string, string)[] targets = Targets; for (int i = 0; i < targets.Length; i++) { (string, string) tuple = targets[i]; string item = tuple.Item1; string item2 = tuple.Item2; Type type = Reflect.TypeByName(item); if (type == null) { Log.Verbose("Guard target type " + item + " is absent on this game version."); continue; } MethodInfo methodInfo2 = Reflect.Method(type, item2); if (methodInfo2 == null || methodInfo2.IsAbstract) { Log.Verbose("Guard target " + item + "." + item2 + " is absent; skipping."); continue; } try { harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(methodInfo), (HarmonyMethod)null); num++; } catch (Exception ex) { Log.Verbose("Could not guard " + item + "." + item2 + ": " + ex.GetType().Name); } } Log.Info("Installed " + num + " of " + Targets.Length + " late-join client guards