using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; using MegaKoop.Game; #if STEAMWORKSNET using Steamworks; #endif namespace MegaKoop.Game.Networking { /// /// Handles transitioning from the lobby into the character scene and spawning player avatars. /// Lives alongside the Steam services object so it survives scene loads. /// [DefaultExecutionOrder(-500)] public class LobbyGameSceneCoordinator : MonoBehaviour { private static LobbyGameSceneCoordinator Instance; [SerializeField] private string characterSceneName = "CharacterScene"; [SerializeField] private float spawnRadius = 3f; [SerializeField] private float minimumSpawnSpacing = 2.5f; private readonly List pendingPlayers = new(); private string localSteamId = string.Empty; private bool loadPending; private bool hasSpawned; private void Awake() { // Singleton to avoid double spawners (e.g., one from DontDestroyOnLoad and one from scene contents) if (Instance != null && Instance != this) { // Another coordinator already exists and persists; remove this duplicate. Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); SceneManager.sceneLoaded += HandleSceneLoaded; } private void OnDestroy() { SceneManager.sceneLoaded -= HandleSceneLoaded; } /// /// Captures the current lobby membership and moves everyone into the character scene. /// public void BeginGame(MegaKoop.Steam.SteamLobbyService steam) { if (steam == null) { Debug.LogWarning("[LobbyGameSceneCoordinator] SteamLobbyService missing; cannot begin game."); return; } CapturePlayers(steam); if (!string.Equals(SceneManager.GetActiveScene().name, characterSceneName, StringComparison.OrdinalIgnoreCase)) { loadPending = true; hasSpawned = false; SceneManager.LoadScene(characterSceneName); } else { // Already in the target scene (e.g., during hot reload). Avoid double-spawn. if (hasSpawned) { Debug.Log("[LobbyGameSceneCoordinator] BeginGame called but players already spawned; ignoring."); return; } loadPending = false; hasSpawned = false; SpawnPlayersInScene(SceneManager.GetActiveScene()); } } private void CapturePlayers(MegaKoop.Steam.SteamLobbyService steam) { pendingPlayers.Clear(); localSteamId = steam.LocalSteamIdString ?? string.Empty; var members = steam.GetMembers(); if (members != null) { foreach (var member in members) { pendingPlayers.Add(new LobbyPlayerInfo { SteamId = member.steamId, DisplayName = string.IsNullOrWhiteSpace(member.name) ? "Player" : member.name, IsLocal = member.steamId == localSteamId }); } } // Ensure the local player is represented even if lobby data is empty (e.g., offline stub). if (!pendingPlayers.Exists(p => p.IsLocal)) { pendingPlayers.Add(new LobbyPlayerInfo { SteamId = localSteamId, DisplayName = GetFallbackLocalName(), IsLocal = true }); } pendingPlayers.Sort((a, b) => string.CompareOrdinal(a.SteamId ?? string.Empty, b.SteamId ?? string.Empty)); } private void HandleSceneLoaded(Scene scene, LoadSceneMode mode) { if (!string.Equals(scene.name, characterSceneName, StringComparison.OrdinalIgnoreCase)) { return; } if (!loadPending && hasSpawned) { return; } loadPending = false; hasSpawned = false; SpawnPlayersInScene(scene); } private void SpawnPlayersInScene(Scene scene) { if (hasSpawned) { return; } var template = FindWizardTemplate(scene); if (template == null) { Debug.LogWarning("[LobbyGameSceneCoordinator] Wizard template not found in CharacterScene; cannot spawn players."); return; } hasSpawned = true; // Proactively remove any previously spawned character clones (e.g., from a duplicate coordinator) DespawnExistingClones(scene, template); Vector3 basePosition = template.transform.position; Quaternion baseRotation = template.transform.rotation; Transform parent = template.transform.parent; template.SetActive(false); int total = Mathf.Max(1, pendingPlayers.Count); for (int i = 0; i < total; i++) { LobbyPlayerInfo info = i < pendingPlayers.Count ? pendingPlayers[i] : new LobbyPlayerInfo { DisplayName = $"Player {i + 1}", IsLocal = i == 0 }; Vector3 spawnPosition = CalculateSpawnPosition(basePosition, i, total); var clone = Instantiate(template, spawnPosition, baseRotation, parent); clone.name = BuildPlayerName(info, i); ConfigureCloneForPlayer(clone, info, i); clone.SetActive(true); } } private static void DespawnExistingClones(Scene scene, GameObject template) { // Destroy any GameObjects in the scene that look like spawned player avatars, excluding the template hierarchy. foreach (var root in scene.GetRootGameObjects()) { // Skip the template root itself if (root == template) continue; // Heuristic: if it has a SteamCharacterNetworkBridge or ThirdPersonCharacterController, treat it as a character clone var bridge = root.GetComponentInChildren(true); var tpc = root.GetComponentInChildren(true); if (bridge != null || tpc != null) { // Avoid deleting the template if nested under a different root (shouldn't happen) if (!template.transform.IsChildOf(root.transform) && !root.transform.IsChildOf(template.transform)) { UnityEngine.Object.Destroy(root); } } } } private static GameObject FindWizardTemplate(Scene scene) { foreach (var root in scene.GetRootGameObjects()) { if (root.name.Equals("Wizard", StringComparison.OrdinalIgnoreCase)) { return root; } var controller = root.GetComponentInChildren(true); if (controller != null) { return controller.gameObject; } } return null; } private Vector3 CalculateSpawnPosition(Vector3 center, int index, int total) { if (total <= 1) { return center; } float radiusForRing = Mathf.Max(spawnRadius, minimumSpawnSpacing) * Mathf.Ceil((index + 1) / 6f); float angle = (Mathf.PI * 2f / Mathf.Max(1, Mathf.Min(total, 6))) * (index % 6); Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radiusForRing; return center + offset; } private void ConfigureCloneForPlayer(GameObject clone, LobbyPlayerInfo info, int index) { // Ensure network identity is deterministic across clients. var identity = clone.GetComponent() ?? clone.AddComponent(); identity.SetNetworkId(index + 1); var bridge = clone.GetComponent() ?? clone.AddComponent(); bridge.AssignOwner(ParseSteamId(info.SteamId), info.IsLocal); var inputSender = clone.GetComponent() ?? clone.AddComponent(); inputSender.enabled = info.IsLocal; // Local player keeps the camera and audio; remote avatars are visual only. var cameras = clone.GetComponentsInChildren(true); foreach (var camera in cameras) { camera.enabled = info.IsLocal; if (!info.IsLocal) { camera.gameObject.SetActive(false); } } var listeners = clone.GetComponentsInChildren(true); foreach (var listener in listeners) { listener.enabled = info.IsLocal; if (!info.IsLocal) { listener.gameObject.SetActive(false); } } var thirdPersonCamera = clone.GetComponentInChildren(true); if (thirdPersonCamera != null) { thirdPersonCamera.gameObject.SetActive(info.IsLocal); thirdPersonCamera.enabled = info.IsLocal; if (info.IsLocal) { thirdPersonCamera.SetTarget(clone.transform); } } var controller = clone.GetComponent(); if (!info.IsLocal && controller != null) { controller.SetCameraTransform(null); } // Ensure only the local player's camera remains active and is MainCamera if (info.IsLocal) { foreach (var cam in cameras) { cam.tag = "MainCamera"; cam.enabled = true; cam.gameObject.SetActive(true); } // Disable any other active cameras that are not part of this player foreach (var otherCam in UnityEngine.Object.FindObjectsOfType()) { if (!otherCam.transform.IsChildOf(clone.transform)) { otherCam.enabled = false; } } // Ensure there is exactly one enabled AudioListener (on this player) foreach (var otherListener in UnityEngine.Object.FindObjectsOfType()) { if (!otherListener.transform.IsChildOf(clone.transform)) { otherListener.enabled = false; } } // Bind the character controller to this camera for input-relative movement if (controller != null) { Transform camTransform = thirdPersonCamera != null ? thirdPersonCamera.transform : (Camera.main != null ? Camera.main.transform : null); if (camTransform != null) { controller.SetCameraTransform(camTransform); } } if (inputSender != null && thirdPersonCamera != null) { inputSender.SetCameraTransform(thirdPersonCamera.transform); } } } private static string BuildPlayerName(LobbyPlayerInfo info, int index) { string baseName = !string.IsNullOrWhiteSpace(info.DisplayName) ? info.DisplayName : $"Player {index + 1}"; return $"Wizard_{baseName}"; } private static string GetFallbackLocalName() { #if STEAMWORKSNET if (SteamBootstrap.IsInitialized) { return SteamFriends.GetPersonaName(); } #endif return "You"; } private static ulong ParseSteamId(string steamId) { if (string.IsNullOrEmpty(steamId)) { return 0; } if (ulong.TryParse(steamId, out var value)) { return value; } return 0; } [Serializable] private struct LobbyPlayerInfo { public string SteamId; public string DisplayName; public bool IsLocal; } } }