using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
using UnityEngine.SceneManagement;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("latejoinfix2")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("latejoinfix2")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("b78d6a4e-bdfc-4a7a-9193-c02df76c2f95")]
[assembly: AssemblyFileVersion("1.4.2.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.4.2.0")]
namespace LateJoinFix;
[BepInPlugin("com.sol.latejoinfix", "LateJoinFix", "1.4.2")]
public class Plugin : BaseUnityPlugin
{
internal sealed class LateJoinListener : MonoBehaviourPunCallbacks
{
private readonly HashSet<string> _knownPlayerKeys = new HashSet<string>();
private readonly HashSet<int> _pendingLateJoinActors = new HashSet<int>();
private readonly HashSet<int> _runningFixActors = new HashSet<int>();
private void Start()
{
CaptureInitialPlayers();
SceneManager.sceneLoaded += OnSceneLoaded;
PlayerHandler.CharacterRegistered = (Action<Character>)Delegate.Combine(PlayerHandler.CharacterRegistered, new Action<Character>(OnCharacterRegistered));
}
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
PlayerHandler.CharacterRegistered = (Action<Character>)Delegate.Remove(PlayerHandler.CharacterRegistered, new Action<Character>(OnCharacterRegistered));
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (((Scene)(ref scene)).name == "Airport")
{
_knownPlayerKeys.Clear();
_pendingLateJoinActors.Clear();
_runningFixActors.Clear();
}
else
{
CaptureInitialPlayers();
}
}
private void CaptureInitialPlayers()
{
//IL_0008: Unknown result type (might be due to invalid IL or missing references)
//IL_000d: Unknown result type (might be due to invalid IL or missing references)
if (!PhotonNetwork.InRoom)
{
return;
}
Scene activeScene = SceneManager.GetActiveScene();
if (((Scene)(ref activeScene)).name == "Airport")
{
return;
}
Player[] playerList = PhotonNetwork.PlayerList;
for (int i = 0; i < playerList.Length; i++)
{
string stablePlayerKey = GetStablePlayerKey(playerList[i]);
if (!string.IsNullOrEmpty(stablePlayerKey))
{
_knownPlayerKeys.Add(stablePlayerKey);
}
}
LogInfo("[LateJoinFix] Initial player snapshot captured. Count = " + _knownPlayerKeys.Count);
}
public override void OnPlayerEnteredRoom(Player newPlayer)
{
//IL_000c: Unknown result type (might be due to invalid IL or missing references)
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
if (newPlayer == null || !PhotonNetwork.InRoom)
{
return;
}
Scene activeScene = SceneManager.GetActiveScene();
if (((Scene)(ref activeScene)).name == "Airport")
{
return;
}
string stablePlayerKey = GetStablePlayerKey(newPlayer);
if (string.IsNullOrEmpty(stablePlayerKey))
{
LogWarning("[LateJoinFix] Failed to build stable player key. Skip.");
return;
}
if (_knownPlayerKeys.Contains(stablePlayerKey))
{
LogInfo("[LateJoinFix] Rejoin/known player detected. Skip late-join handling for: " + newPlayer.NickName + " / " + stablePlayerKey);
return;
}
_knownPlayerKeys.Add(stablePlayerKey);
_pendingLateJoinActors.Add(newPlayer.ActorNumber);
LogInfo("[LateJoinFix] True late join detected: " + newPlayer.NickName + " / " + newPlayer.ActorNumber);
((MonoBehaviour)this).StartCoroutine(RemovePendingIfNeverRegistered(newPlayer.ActorNumber, 30f));
if (PhotonNetwork.IsMasterClient)
{
((MonoBehaviour)this).StartCoroutine(HandleLateJoin(newPlayer));
}
}
private IEnumerator RemovePendingIfNeverRegistered(int actorNumber, float timeout)
{
yield return (object)new WaitForSeconds(timeout);
_pendingLateJoinActors.Remove(actorNumber);
_runningFixActors.Remove(actorNumber);
}
private void OnCharacterRegistered(Character character)
{
if ((Object)(object)character == (Object)null || (Object)(object)character.data == (Object)null || (Object)(object)((MonoBehaviourPun)character).photonView == (Object)null)
{
return;
}
Player owner = ((MonoBehaviourPun)character).photonView.Owner;
if (owner != null)
{
int actorNumber = owner.ActorNumber;
if (_pendingLateJoinActors.Contains(actorNumber) && !_runningFixActors.Contains(actorNumber))
{
((MonoBehaviour)this).StartCoroutine(FixAliveStateRoutine(character, actorNumber));
}
}
}
private IEnumerator HandleLateJoin(Player newPlayer)
{
yield return (object)new WaitForSeconds(0.2f);
Character newCharacter = null;
for (float elapsed = 0f; elapsed < 30f; elapsed += 0.1f)
{
try
{
newCharacter = PlayerHandler.GetPlayerCharacter(newPlayer);
}
catch (Exception ex)
{
LogWarning("[LateJoinFix] GetPlayerCharacter exception: " + ex.Message);
}
if ((Object)(object)newCharacter != (Object)null)
{
break;
}
yield return (object)new WaitForSeconds(0.1f);
}
if ((Object)(object)newCharacter == (Object)null)
{
LogError("[LateJoinFix] Failed to find new character.");
yield break;
}
yield return (object)new WaitForSeconds(0.1f);
ImprovedSpawnTarget spawnTarget = PopulateSpawnData(newCharacter);
if ((Object)(object)spawnTarget.LowestCharacter == (Object)null)
{
LogError("[LateJoinFix] No valid spawn target found.");
yield break;
}
try
{
Vector3 val = spawnTarget.LowestCharacter.Center + Vector3.forward * WarpForwardOffset.Value;
((MonoBehaviourPun)newCharacter).photonView.RPC("WarpPlayerRPC", (RpcTarget)0, new object[2] { val, false });
}
catch (Exception ex2)
{
LogWarning("[LateJoinFix] WarpPlayerRPC failed: " + ex2.Message);
}
yield return (object)new WaitForSeconds(AfterWarpDelay.Value);
try
{
Vector3 val2 = spawnTarget.LowestCharacter.Center + new Vector3(0f, ReviveHeightOffset.Value, 0f);
((MonoBehaviourPun)newCharacter).photonView.RPC("RPCA_ReviveAtPosition", (RpcTarget)0, new object[3] { val2, false, -1 });
}
catch (Exception ex3)
{
LogWarning("[LateJoinFix] RPCA_ReviveAtPosition failed: " + ex3.Message);
}
}
private IEnumerator FixAliveStateRoutine(Character character, int actorNumber)
{
_runningFixActors.Add(actorNumber);
yield return (object)new WaitForSeconds(1.5f);
for (int i = 0; i < 12; i++)
{
if ((Object)(object)character == (Object)null)
{
break;
}
if ((Object)(object)character.data == (Object)null)
{
break;
}
character.data.dead = false;
character.data.passedOut = false;
character.data.fullyPassedOut = false;
character.data.deathTimer = 0f;
yield return (object)new WaitForSeconds(0.25f);
}
_pendingLateJoinActors.Remove(actorNumber);
_runningFixActors.Remove(actorNumber);
LogInfo("[LateJoinFix] Cleared revive/death state for actor " + actorNumber);
}
private static ImprovedSpawnTarget PopulateSpawnData(Character newCharacter)
{
ImprovedSpawnTarget improvedSpawnTarget = new ImprovedSpawnTarget();
foreach (Character allCharacter in Character.AllCharacters)
{
if (!((Object)(object)allCharacter == (Object)null) && !((Object)(object)allCharacter.data == (Object)null) && !((Object)(object)allCharacter == (Object)(object)newCharacter) && !allCharacter.data.dead)
{
improvedSpawnTarget.RegisterCharacter(allCharacter);
}
}
return improvedSpawnTarget;
}
private static string GetStablePlayerKey(Player player)
{
if (player == null)
{
return string.Empty;
}
if (!string.IsNullOrEmpty(player.UserId))
{
return "USERID:" + player.UserId;
}
if (!string.IsNullOrEmpty(player.NickName))
{
return "NICK:" + player.NickName;
}
return "ACTOR:" + player.ActorNumber;
}
}
public class ImprovedSpawnTarget
{
private float lowestClimbed = float.MaxValue;
public Character LowestCharacter;
public void RegisterCharacter(Character character)
{
//IL_000b: 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)
if (!((Object)(object)character == (Object)null) && character.Center.y < lowestClimbed)
{
lowestClimbed = character.Center.y;
LowestCharacter = character;
}
}
}
internal static ManualLogSource Log;
internal static ConfigEntry<float> AfterWarpDelay;
internal static ConfigEntry<float> WarpForwardOffset;
internal static ConfigEntry<float> ReviveHeightOffset;
private const float InitialDelayAfterJoinEvent = 0.2f;
private const float CharacterSearchInterval = 0.1f;
private const float AfterCharacterFoundDelay = 0.1f;
private const float CharacterSearchTimeoutSeconds = 30f;
private LateJoinListener _listener;
private void Awake()
{
Log = ((BaseUnityPlugin)this).Logger;
AfterWarpDelay = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "AfterWarpDelay", 2f, "워프 후 부활 RPC를 보내기 전 대기 시간(초).");
WarpForwardOffset = ((BaseUnityPlugin)this).Config.Bind<float>("Position", "WarpForwardOffset", 2f, "대상 플레이어 위치에서 월드 전방(Vector3.forward)으로 비껴서 워프할 거리.");
ReviveHeightOffset = ((BaseUnityPlugin)this).Config.Bind<float>("Position", "ReviveHeightOffset", 3f, "대상 플레이어 Center 기준 부활 위치의 높이 오프셋.");
_listener = ((Component)this).gameObject.AddComponent<LateJoinListener>();
((BaseUnityPlugin)this).Logger.LogInfo((object)"LateJoinFix loaded.");
}
internal static void LogInfo(string msg)
{
Log.LogInfo((object)msg);
}
internal static void LogWarning(string msg)
{
Log.LogWarning((object)msg);
}
internal static void LogError(string msg)
{
Log.LogError((object)msg);
}
}