using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("Rombusz")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Reduces PEAK character lifecycle hitches caused by initialization races and repeated diagnostic exceptions.")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyInformationalVersion("2.0.0")]
[assembly: AssemblyProduct("CrossplayStutterFix")]
[assembly: AssemblyTitle("CrossplayStutterFix")]
[assembly: AssemblyVersion("2.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
[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 CrossplayStutterFix
{
[BepInPlugin("Rombusz.CrossplayStutterFix", "CrossplayStutterFix", "2.0.0")]
public sealed class Plugin : BaseUnityPlugin
{
public const string Guid = "Rombusz.CrossplayStutterFix";
public const string Name = "CrossplayStutterFix";
public const string Version = "2.0.0";
internal static ManualLogSource? Log;
private Harmony? _harmony;
private void Awake()
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
//IL_001b: Expected O, but got Unknown
Log = ((BaseUnityPlugin)this).Logger;
_harmony = new Harmony("Rombusz.CrossplayStutterFix");
Type[] types = typeof(Plugin).Assembly.GetTypes();
foreach (Type type in types)
{
if (Attribute.IsDefined(type, typeof(HarmonyPatch)))
{
try
{
_harmony.CreateClassProcessor(type).Patch();
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch " + type.Name + " could not be installed: " + ex.GetBaseException().Message));
}
}
}
((BaseUnityPlugin)this).Logger.LogMessage((object)"Plugin CrossplayStutterFix 2.0.0 is loaded!");
}
private void Update()
{
LifecycleDiagnostics.Tick();
}
private void OnDestroy()
{
Harmony? harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
}
[HarmonyPatch(typeof(Character), "WarpPlayer")]
internal static class CharacterWarpPatch
{
private static readonly MethodInfo StopClimbing = AccessTools.Method(typeof(CharacterClimbing), "StopAnyClimbing", (Type[])null, (Type[])null);
private static readonly MethodInfo SafeStopClimbing = AccessTools.Method(typeof(CharacterWarpPatch), "StopClimbingWhenReady", (Type[])null, (Type[])null);
private static readonly MethodInfo LogError = AccessTools.Method(typeof(Debug), "LogError", new Type[1] { typeof(object) }, (Type[])null);
private static readonly FieldRef<CharacterClimbing, Character>? ClimbingCharacter = SafeBindings.Field<CharacterClimbing, Character>("character");
private static readonly MethodInfo Warn = AccessTools.Method(typeof(LifecycleDiagnostics), "WarpWarning", (Type[])null, (Type[])null);
[HarmonyPrepare]
private static bool Prepare()
{
if (ClimbingCharacter != null && StopClimbing != null)
{
return SafeStopClimbing != null;
}
return false;
}
[HarmonyPrefix]
private static void TraceOverlap(Character __instance, Vector3 position)
{
//IL_005a: Unknown result type (might be due to invalid IL or missing references)
if (__instance.warping)
{
PhotonView photonView = ((MonoBehaviourPun)__instance).photonView;
object arg = ((photonView != null) ? new int?(photonView.ViewID) : ((int?)null));
CharacterData data = __instance.data;
LifecycleDiagnostics.Warn("warp-overlap", $"Overlapping warp: view={arg}, dead={((data != null) ? new bool?(data.dead) : ((bool?)null))}, target={position}. Vanilla will stop its previous warp routine.");
}
}
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> GuardEarlyWarp(IEnumerable<CodeInstruction> instructions)
{
int climbingMatches = 0;
foreach (CodeInstruction instruction in instructions)
{
if (CodeInstructionExtensions.Calls(instruction, StopClimbing))
{
instruction.opcode = OpCodes.Call;
instruction.operand = SafeStopClimbing;
climbingMatches++;
}
else if (CodeInstructionExtensions.Calls(instruction, LogError))
{
instruction.opcode = OpCodes.Call;
instruction.operand = Warn;
}
yield return instruction;
}
if (climbingMatches == 0)
{
ManualLogSource? log = Plugin.Log;
if (log != null)
{
log.LogWarning((object)"Warp climbing guard found no matching call; game code may have changed.");
}
}
}
private static void StopClimbingWhenReady(CharacterClimbing climbing)
{
if ((Object)(object)climbing != (Object)null && (Object)(object)ClimbingCharacter.Invoke(climbing) != (Object)null)
{
climbing.StopAnyClimbing();
}
}
}
[HarmonyPatch(typeof(Dynamite), "TestLightWick")]
internal static class DynamiteProximityPatch
{
private static Character? _readyCharacter;
[HarmonyPrefix]
private static bool RunWhenLocalCharacterIsUsable()
{
Character localCharacter = Character.localCharacter;
if ((Object)(object)localCharacter == (Object)null)
{
return false;
}
if ((Object)(object)localCharacter == (Object)(object)_readyCharacter)
{
return true;
}
if (localCharacter.refs != null && (Object)(object)localCharacter.refs.ragdoll != (Object)null && localCharacter.refs.ragdoll.partDict.TryGetValue((BodypartType)2, out var value) && (Object)(object)value != (Object)null && (Object)(object)value.transform != (Object)null)
{
_readyCharacter = localCharacter;
return true;
}
return false;
}
}
[HarmonyPatch(typeof(Character), "RPCA_Fall")]
internal static class FallDiagnosticPatch
{
private static readonly MethodInfo Log = AccessTools.Method(typeof(Debug), "Log", new Type[1] { typeof(object) }, (Type[])null);
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> RemoveRepeatedFallLog(IEnumerable<CodeInstruction> instructions)
{
int matches = 0;
foreach (CodeInstruction instruction in instructions)
{
if (CodeInstructionExtensions.Calls(instruction, Log))
{
matches++;
instruction.opcode = OpCodes.Pop;
instruction.operand = null;
}
yield return instruction;
}
if (matches == 0)
{
ManualLogSource? log = Plugin.Log;
if (log != null)
{
log.LogInfo((object)"Fall log suppression: no matching log call (possibly a log-stripped game build).");
}
}
}
}
[HarmonyPatch(typeof(CharacterStats), "Record")]
internal static class ReviveTimelineDiagnosticPatch
{
private static readonly MethodInfo LogError = AccessTools.Method(typeof(Debug), "LogError", new Type[1] { typeof(object) }, (Type[])null);
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> RemoveReviveErrorStack(IEnumerable<CodeInstruction> instructions)
{
int matches = 0;
foreach (CodeInstruction instruction in instructions)
{
if (CodeInstructionExtensions.Calls(instruction, LogError))
{
matches++;
instruction.opcode = OpCodes.Pop;
instruction.operand = null;
}
yield return instruction;
}
if (matches == 0)
{
ManualLogSource? log = Plugin.Log;
if (log != null)
{
log.LogInfo((object)"Revive log suppression: no matching error-log call (possibly a log-stripped game build).");
}
}
}
}
internal static class SafeBindings
{
internal static FieldRef<T, F>? Field<T, F>(string name) where T : class
{
try
{
return AccessTools.FieldRefAccess<T, F>(name);
}
catch (Exception ex)
{
ManualLogSource? log = Plugin.Log;
if (log != null)
{
log.LogWarning((object)("Disabled dependent patches: cannot bind " + typeof(T).Name + "." + name + ": " + ex.Message));
}
return null;
}
}
}
[HarmonyPatch(typeof(CharacterSpawner), "RPC_NewPlayerSpawn")]
internal static class LateJoinSpawnPatch
{
private sealed class Flight
{
internal bool Running;
}
private static readonly ConditionalWeakTable<MonoBehaviour, Flight> Flights = new ConditionalWeakTable<MonoBehaviour, Flight>();
private static readonly FieldRef<CharacterSpawner, bool>? HasSpawned = SafeBindings.Field<CharacterSpawner, bool>("hasSpawnedPlayer");
private const float ReadinessTimeout = 120f;
[HarmonyPrepare]
private static bool Prepare()
{
return HasSpawned != null;
}
[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> WrapSpawnRoutine(IEnumerable<CodeInstruction> instructions)
{
//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
//IL_00dd: Expected O, but got Unknown
//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
//IL_00f0: Expected O, but got Unknown
//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
//IL_00fe: Expected O, but got Unknown
MethodInfo start = AccessTools.Method(typeof(MonoBehaviour), "StartCoroutine", new Type[1] { typeof(IEnumerator) }, (Type[])null);
MethodInfo methodInfo = AccessTools.Method(typeof(LateJoinSpawnPatch), "StartOnce", (Type[])null, (Type[])null);
List<CodeInstruction> list = new List<CodeInstruction>(instructions);
int count = list.FindAll((CodeInstruction i) => CodeInstructionExtensions.Calls(i, start)).Count;
if (count != 1)
{
ManualLogSource? log = Plugin.Log;
if (log != null)
{
log.LogWarning((object)$"Spawn coroutine guard not installed: expected one StartCoroutine call, found {count}. Vanilla spawning retained.");
}
return list;
}
int num = list.FindIndex((CodeInstruction i) => CodeInstructionExtensions.Calls(i, start));
list[num].opcode = OpCodes.Ldarg_1;
list[num].operand = null;
list.InsertRange(num + 1, (IEnumerable<CodeInstruction>)(object)new CodeInstruction[3]
{
new CodeInstruction(OpCodes.Ldarg_3, (object)null),
new CodeInstruction(OpCodes.Ldarg_S, (object)(byte)4),
new CodeInstruction(OpCodes.Call, (object)methodInfo)
});
return list;
}
private static Coroutine? StartOnce(CharacterSpawner spawner, IEnumerator routine, Segment currentMapStage, int lastReviveSegment, PhotonMessageInfo message)
{
//IL_0041: 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)
Flight orCreateValue = Flights.GetOrCreateValue((MonoBehaviour)(object)spawner);
if (orCreateValue.Running)
{
(routine as IDisposable)?.Dispose();
LifecycleDiagnostics.Warn("spawn-duplicate", "Ignored duplicate late-join spawn request while its original coroutine is still running.");
return null;
}
orCreateValue.Running = true;
try
{
return ((MonoBehaviour)spawner).StartCoroutine(Run(spawner, routine, orCreateValue, currentMapStage, lastReviveSegment, message.Sender));
}
catch
{
orCreateValue.Running = false;
(routine as IDisposable)?.Dispose();
throw;
}
}
private static IEnumerator Run(CharacterSpawner spawner, IEnumerator routine, Flight flight, Segment currentMapStage, int lastReviveSegment, Player sender)
{
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_001d: Unknown result type (might be due to invalid IL or missing references)
Room room = PhotonNetwork.CurrentRoom;
bool needsIsland = (int)currentMapStage != 0 || lastReviveSegment != int.MinValue;
float started = Time.realtimeSinceStartup;
bool waited = false;
try
{
while (!((Object)(object)spawner == (Object)null) && ((Behaviour)spawner).isActiveAndEnabled && !HasSpawned.Invoke(spawner) && room != null && room == PhotonNetwork.CurrentRoom && sender != null && sender.IsMasterClient)
{
needsIsland |= GameHandler.IsOnIsland || ((Object)(object)GameHandler.Instance != (Object)null && GameHandler.IsInGameplayScene);
if (!(needsIsland ? GameHandler.IsOnIslandAndInitialized : ((Object)(object)GameHandler.Instance != (Object)null && GameHandler.IsInMultiplayerScene)))
{
if (Time.realtimeSinceStartup - started >= 120f)
{
LifecycleDiagnostics.Warn("spawn-map-timeout", "Late-join spawn timed out waiting for scene/map readiness; no airport fallback was attempted. A fresh host request can retry.");
break;
}
if (!waited)
{
waited = true;
LifecycleDiagnostics.Warn("spawn-map-wait", $"Deferring late-join spawn until scene/map initialization is ready (segment={currentMapStage}, lastRevive={lastReviveSegment}).");
}
yield return null;
}
else
{
if (waited)
{
LifecycleDiagnostics.Warn("spawn-map-ready", "Scene/map initialized; resuming the original late-join spawn routine.");
waited = false;
}
if (!routine.MoveNext())
{
break;
}
yield return routine.Current;
}
}
}
finally
{
flight.Running = false;
(routine as IDisposable)?.Dispose();
}
}
}
internal static class LifecycleDiagnostics
{
private static readonly Dictionary<string, float> LastWarnings = new Dictionary<string, float>();
private static Character? _lastCharacter;
private static bool _lastDead;
private static float _nextCheck;
internal static void Warn(string key, string message)
{
float realtimeSinceStartup = Time.realtimeSinceStartup;
if (!LastWarnings.TryGetValue(key, out var value) || !(realtimeSinceStartup - value < 5f))
{
LastWarnings[key] = realtimeSinceStartup;
ManualLogSource? log = Plugin.Log;
if (log != null)
{
log.LogWarning((object)message);
}
}
}
internal static void WarpWarning(object message)
{
Warn("warp-overlap", message?.ToString() ?? "Overlapping warp.");
}
internal static void Tick()
{
//IL_011e: Unknown result type (might be due to invalid IL or missing references)
if (Time.realtimeSinceStartup < _nextCheck)
{
return;
}
_nextCheck = Time.realtimeSinceStartup + 1f;
Character localCharacter = Character.localCharacter;
if ((Object)(object)localCharacter == (Object)null)
{
_lastCharacter = null;
}
else
{
if ((Object)(object)localCharacter.data == (Object)null || (Object)(object)localCharacter.refs?.ragdoll == (Object)null || !localCharacter.refs.ragdoll.partDict.TryGetValue((BodypartType)2, out var value) || (Object)(object)value == (Object)null || (Object)(object)value.transform == (Object)null)
{
return;
}
bool dead = localCharacter.data.dead;
if ((Object)(object)_lastCharacter != (Object)(object)localCharacter || dead != _lastDead)
{
ManualLogSource? log = Plugin.Log;
if (log != null)
{
object[] array = new object[5];
PhotonView photonView = ((MonoBehaviourPun)localCharacter).photonView;
array[0] = ((photonView != null) ? new int?(photonView.ViewID) : ((int?)null));
array[1] = dead;
array[2] = localCharacter.data.fullyPassedOut;
array[3] = localCharacter.warping;
array[4] = localCharacter.Center;
log.LogInfo((object)(string.Format("Local lifecycle: view={0}, dead={1}, fullyPassedOut={2}, warping={3}, position={4}.", array) + (dead ? " Vanilla FixedUpdate pulls dead characters toward (0, 5000, -5000); this message does not mean death was illegitimate." : string.Empty)));
}
_lastCharacter = localCharacter;
_lastDead = dead;
}
}
}
}
}