Creating an offline LAN multiplayer game using Netcode for GameObjects/Relay

I’m creating a multiplayer game for our school project and I wanted it to work even without access to the internet, I heard that Relay can be used to creating one but I don’t know where to start/ how to make it to work offline as it needs to access authentication first which requires internet connection to perform.

Our game will be run on android devices.

Hi,

You can find the answer on the FAQ from the Netcode documentation : Frequently asked questions - How do I join from two devices on the same network? | Unity Multiplayer Networking (unity3d.com)
Clients need to connect to the local IPv4 address of the server.
You can do it inside Unity by retrieving your local IP address and display it on screen on your server. (There is a way to find your local IP address in C# by using System.Net.Dns class, like used inside this thread : Get the device IP address from Unity - Questions & Answers - Unity Discussions)
This way, just like you’d do it on Relay, you will be able to share your server “join code” (i.e. the IP address) to the clients, and inside the game, you wanna set the transport layer to UnityTransport and use method like SetConnectionData to change the IP address to where the client should connect.
I don’t have any information about Unity Relay working offline, and it seems to me like it wouldn’t work just like you mentioned that you can’t access it nor Authentication.

If you want server/client(s) to find themselves on LAN without entering the IP address, I suggest you to search Network Discovery, which can found be on Netcode via the community contributions :
GitHub - Unity-Technologies/multiplayer-community-contributions: Community contributions to Unity Multiplayer Networking products and services.

Hope this helps, please ask for more information if you need :slightly_smiling_face:

Will this configuration work on android devices? I forgot to mention that the game that I am creating is a mobile game

I have never used Netcode for a Mobile project so I won’t be able to tell you exactly.
All I can say is that from what I found on forums, getting the IP address on Android is not as easy as it can be on Windows. I have found this thread where someone tried to get the IP address on an Android tablet : Can’t get Android’s (tablet) Local IP address - Unity Engine - Unity Discussions, so maybe you can make something work on your Android setup.
Other than that, setting the connection Data inside the Transport of the NetworkManager is basic Netcode, which is supported on Android so it should work.
I can’t tell you how Android devices will act when using the Network Discovery feature, I guess best is to just try it out if you’ve got the time and need to do it :slightly_smiling_face:

But are there any multiplayer solutions that can be used/work normally on mobile devices and is able to be used without any connection to the internet? Sorry if I ask so many questions, it has been only a few months since I started using unity and it’s my first time creating a game

Don’t worry about asking questions, that’s what Unity discussions is for :slight_smile:
In order for your server and clients to be able to join automatically on LAN, this is what Network Discovery is for. This is available as a community contributions inside Netcode, so you can use it :

I can’t tell you for sure if it works on mobile devices (I want to say yes but not sure), so you might just try it and see by yourself !
Fish-Net is a networking solution available for Unity and has a Network Discovery feature that IS “Fully Supported” on Android : Fish-Network-Discovery | Fish-Net: Networking Evolved (gitbook.io), so this is can be an alternative solution

I came across this a year later through Google, and I thought I would add to it. I found that the community contributions Network Discovery did not work with android. I think it needs ipv4, and I think android addresses need to be in ipv6 format.

My memory is a bit fuzzy on it, but in UNET we used an ipv4 broadcast event, and then converted that to ipv6 and it would work. IPv6 doesn’t have the same options for discovery because the address space is much bigger, so you have to do it a different way. The community package doesn’t really do that that I can see. I could not get it to work on android.

2 Likes

Hello there has anyone managed to find a solution for this issue, as i to am having issue with lan multiplayer for mobile devices ive tried the network discovery contribution and yeah it dosent work, my current impl almost works but dosent at the same time, as the client can visually see the data the host provides for the offline lobby however when the client tries to connect it fails and due to certain reasons i cant debug it the lobby card and data are visible but connection is never established propperly ill provide the code im using if needed so please if anyone can assist any help is welcome

using Random = UnityEngine.Random;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using MidManStudio.Core.HelperFunctions;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Net.NetworkInformation;
using System.Linq;
using MidManStudio.NetworkScene;
using MidManStudio.SceneManagement;
using Unity.Collections;
namespace MidManStudio.OfflineLobby
{
    [System.Serializable]
    public class OfflineLobbyData
    {
        public string LobbyName;
        public string HostName;
        public string HostAddress;
        public int Port;
        public int CurrentPlayers;
        public int MaxPlayers;
        public string GameMode;
        public OfflineGameMode GameModeEnum;
        public OfflineGameMap GameMapEnum;
        public float LastDiscoveryTime;
        public float TimeoutTime;
    }

    [Serializable]
    public class PlayerInfo
    {
        public ulong ClientId;
        public string PlayerName;
        public string PlayerIconId;
        public bool IsReady;
        public bool IsHost;
        public bool IsBot;

        public PlayerInfo(ulong clientId, string playerName, bool isHost = false, bool isBot = false)
        {
            ClientId = clientId;
            PlayerName = playerName;
            IsReady = false;
            IsHost = isHost;
            IsBot = isBot;
        }
    }

    [Serializable]
    public enum OfflineGameMode
    {
        Random = 0,
        TeamDeathMatch = 1,
        HardPoint = 2,
        Objectives = 3,
        IgniteDelivery = 4,
        Survival = 6,
    }

    [Serializable]
    public enum OfflineGameMap
    {
        Random = 0,
        GrassyLand = 1,
        TakiLand = 2,
        CrystalCavern = 3,
        TheFall = 4,
        BlackRock = 5,
        Base =6
    }

    //Network serializable player data 
    public struct NetworkPlayerData : INetworkSerializable, IEquatable<NetworkPlayerData>
    {
        public ulong ClientId;
        public FixedString512Bytes PlayerName;
        public bool IsReady;
        public bool IsHost;

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref ClientId);
            serializer.SerializeValue(ref PlayerName);
            serializer.SerializeValue(ref IsReady);
            serializer.SerializeValue(ref IsHost);
        }

        // REQUIRED: IEquatable implementation for NetworkList
        public bool Equals(NetworkPlayerData other)
        {
            return ClientId == other.ClientId &&
                   PlayerName.Equals(other.PlayerName) &&
                   IsReady == other.IsReady &&
                   IsHost == other.IsHost;
        }

        public override bool Equals(object obj)
        {
            return obj is NetworkPlayerData other && Equals(other);
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(ClientId, PlayerName, IsReady, IsHost);
        }
    }

    public class OfflineLobbyManager : NetworkBehaviour
    {
        [Header("Network Configuration")]
        [SerializeField] private NetworkManager networkManager;
        [SerializeField] private UnityTransport unityTransport;
        [SerializeField] private int serverPort = 7777;
        [SerializeField] private int broadcastPort = 7778;
        [SerializeField] private float discoveryInterval = 1.0f;
        [SerializeField] private float lobbyTimeout = 5.0f;
        [SerializeField] private bool isTestMode = false;

        [Header("Lobby Settings")]
        [SerializeField] private string defaultLobbyName = "Local Game";
        [SerializeField] private int defaultMaxPlayers = 4;
        [SerializeField] private OfflineGameMode defaultGameMode = OfflineGameMode.TeamDeathMatch;
        [SerializeField] private OfflineGameMap defaultGameMap = OfflineGameMap.GrassyLand;

        [Header("Bot Configuration")]
        [SerializeField] private bool fillPlayerSpaces = false;
        [SerializeField] private int maxBots = 4;
        [SerializeField] private string[] botNamePrefixes = new string[] { "Player", "Gamer", "Pro", "Noob", "Legend", "Bot" };
        [SerializeField] private string[] botNameSuffixes = new string[] { "123", "007", "42", "99", "Elite", "Master" };

        public Action<string> OnNetworkStatusChanged;

        // Current lobby state
        private bool _isHosting = false;
        private bool _isSearching = false;
        private bool _isInitialized = false;
        private bool _isShuttingDown = false;
        private UdpClient _udpServer;
        private UdpClient _udpClient;
        private Dictionary<string, OfflineLobbyData> _discoveredLobbies = new Dictionary<string, OfflineLobbyData>();
        private List<PlayerInfo> _currentLobbyPlayers = new List<PlayerInfo>();
        private string _playerName = "Player";
        private OfflineLobbyData _currentLobbyData;
        private OfflineGameMode _selectedGameMode;
        private OfflineGameMap _selectedGameMap;

        // FIXED: Network synced player list with proper initialization
        private NetworkList<NetworkPlayerData> _networkPlayers;

        public Action<OfflineLobbyData> OnLobbyDiscovered;
        public Action<string> OnLobbyRemoved;
        public Action<PlayerInfo> OnPlayerJoined;
        public Action<ulong> OnPlayerLeft;
        public Action<PlayerInfo> OnPlayerReadyStatusChanged;
        public Action<bool> OnJoinLobbyResult;
        public Action<bool> OnHostLobbyResult;
        public Action OnLobbyDisbanded;
        public Action OnLobbyReset;

        private Dictionary<OfflineGameMode, int> _gameModeMaxPlayers = new Dictionary<OfflineGameMode, int>();

        // Singleton instance
        private static OfflineLobbyManager _instance;
        public static OfflineLobbyManager Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = FindAnyObjectByType<OfflineLobbyManager>();
                }
                return _instance;
            }
        }

        #region Unity Lifecycle
        private void Awake()
        {
            
            // Setup singleton
            if (_instance != null && _instance != this)
            {
                Destroy(gameObject);
                return;
            }
            _instance = this;

            // Dont forget to Initialize NetworkList in Awake before any network operations idiot
            _networkPlayers = new NetworkList<NetworkPlayerData>();

            StartCoroutine(InitializeAsync());
        }

        private IEnumerator InitializeAsync()
        {
            yield return new WaitForSeconds(0.1f);

            try
            {
                InitializeComponents();
                InitializeGameModes();
                LoadPlayerName();

                if (!ValidateNetworkManagerConfiguration())
                {
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                        "NetworkManager configuration is invalid. Please check NetworkPrefabs list.");
                    yield break;
                }

                RegisterNetworkCallbacks();

                _isInitialized = true;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "OfflineLobbyManager initialized successfully");
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Failed to initialize OfflineLobbyManager: {e.Message}", e);
            }
        }

        public override void OnNetworkSpawn()
        {
            base.OnNetworkSpawn();

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" OfflineLobbyManager spawned on network! IsServer: {IsServer}, IsClient: {IsClient}, " +
                $"IsHost: {IsHost()}, ClientId: {NetworkManager.LocalClientId}");

            if (IsServer)
            {
                _networkPlayers.OnListChanged += OnNetworkPlayersChanged;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    "Server: Network player list initialized");
            }
            else
            {
                // Client also needs to listen for changes
                _networkPlayers.OnListChanged += OnNetworkPlayersChanged;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    "Client: Listening to network player list changes");
            }
        }

        public override void OnNetworkDespawn()
        {
            if (_networkPlayers != null)
            {
                _networkPlayers.OnListChanged -= OnNetworkPlayersChanged;
            }
            base.OnNetworkDespawn();
        }

        private void OnNetworkPlayersChanged(NetworkListEvent<NetworkPlayerData> changeEvent)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $"📡 NetworkList changed: {changeEvent.Type}, Index: {changeEvent.Index}");

            // Both server and client should sync when list changes
            if (!IsServer)
            {
                // Client: Always rebuild from NetworkList when changes occur
                SyncPlayerListFromNetwork();
            }
            else
            {
                // Server: Update local list to match NetworkList
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    $"Server NetworkList changed - Count: {_networkPlayers.Count}");
            }
        }

        private void SyncPlayerListFromNetwork()
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" Syncing player list from NetworkList. Count: {_networkPlayers.Count}");

            // Clear current list (only real players, not bots)
            _currentLobbyPlayers.RemoveAll(p => !p.IsBot);

            foreach (var netPlayer in _networkPlayers)
            {
                PlayerInfo playerInfo = new PlayerInfo(
                    netPlayer.ClientId,
                    netPlayer.PlayerName.ToString(),
                    netPlayer.IsHost,
                    false
                );
                playerInfo.IsReady = netPlayer.IsReady;

                // Check if player already exists
                if (!_currentLobbyPlayers.Any(p => p.ClientId == netPlayer.ClientId))
                {
                    _currentLobbyPlayers.Add(playerInfo);
                    OnPlayerJoined?.Invoke(playerInfo);
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                        $" Added player from NetworkList: {playerInfo.PlayerName} (ID: {playerInfo.ClientId})");
                }
                else
                {
                    //  Update existing player's ready status
                    var existingPlayer = _currentLobbyPlayers.Find(p => p.ClientId == netPlayer.ClientId);
                    if (existingPlayer != null && existingPlayer.IsReady != netPlayer.IsReady)
                    {
                        existingPlayer.IsReady = netPlayer.IsReady;
                        OnPlayerReadyStatusChanged?.Invoke(existingPlayer);
                    }
                }
            }

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" Synced {_currentLobbyPlayers.Count(p => !p.IsBot)} real players from NetworkList");
        }

        private void InitializeComponents()
        {
            if (networkManager == null)
                networkManager = FindAnyObjectByType<NetworkManager>();

            if (unityTransport == null && networkManager != null)
                unityTransport = networkManager.GetComponent<UnityTransport>();

            if (isTestMode && Application.isEditor)
            {
                int instanceId = gameObject.GetInstanceID() % 100;
                if (instanceId < 0) instanceId = -instanceId;
                serverPort += instanceId;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Test Mode: Server Port: {serverPort}");
            }
        }

        private void LoadPlayerName()
        {
            //oh yeah use our superior bot name generatior from online lobby here tooo.
            _playerName = PlayerPrefs.GetString("PlayerName", "Player");
            if (string.IsNullOrEmpty(_playerName))
            {
                _playerName = "Player";
            }
        }

        private bool ValidateNetworkManagerConfiguration()
        {
            if (networkManager == null)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, "NetworkManager is null");
                return false;
            }

            if (networkManager.NetworkConfig?.Prefabs == null)
            {
                if (networkManager.NetworkConfig == null)
                {
                    networkManager.NetworkConfig = new NetworkConfig();
                }
                networkManager.NetworkConfig.Prefabs = new NetworkPrefabs();
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Created new NetworkPrefabs list");
            }

            if (networkManager.NetworkConfig.Prefabs.NetworkPrefabsLists != null)
            {
                for (int i = networkManager.NetworkConfig.Prefabs.NetworkPrefabsLists.Count - 1; i >= 0; i--)
                {
                    if (networkManager.NetworkConfig.Prefabs.NetworkPrefabsLists[i] == null)
                    {
                        networkManager.NetworkConfig.Prefabs.NetworkPrefabsLists.RemoveAt(i);
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Removed null NetworkPrefabsList at index {i}");
                    }
                }
            }

            return true;
        }

        public override void OnDestroy()
        {
            if (!_isInitialized) return;

            _isShuttingDown = true;

            UnregisterNetworkCallbacks();
            StopDiscoveryServer();
            StopDiscoveryClient();

            _currentLobbyPlayers.Clear();
            _discoveredLobbies.Clear();

            StartCoroutine(SafeShutdownCoroutine());
        }

        private IEnumerator SafeShutdownCoroutine()
        {
            if (networkManager != null && (networkManager.IsConnectedClient || networkManager.IsHost || networkManager.IsServer))
            {
               
                    if (networkManager.IsServer && networkManager.ConnectedClientsIds != null)
                    {
                        var clientIds = new List<ulong>(networkManager.ConnectedClientsIds);
                        foreach (var clientId in clientIds)
                        {
                            if (clientId != networkManager.LocalClientId)
                            {
                                networkManager.DisconnectClient(clientId);
                            }
                        }
                    }

                    yield return new WaitForSeconds(0.2f);

                    networkManager.Shutdown();

                    yield return new WaitForSeconds(0.3f);
            
            }

            _isHosting = false;
            _isSearching = false;
            _currentLobbyData = null;
        }

        private void Update()
        {
            if (!_isInitialized || _isShuttingDown) return;
            CleanupTimedOutLobbies();
        }
        #endregion

        #region Network Callbacks
        private void RegisterNetworkCallbacks()
        {
            if (networkManager != null)
            {
                networkManager.OnClientConnectedCallback += OnClientConnected;
                networkManager.OnClientDisconnectCallback += OnClientDisconnected;
            }
        }

        private void UnregisterNetworkCallbacks()
        {
            if (networkManager != null)
            {
                networkManager.OnClientConnectedCallback -= OnClientConnected;
                networkManager.OnClientDisconnectCallback -= OnClientDisconnected;
            }
        }

        private void OnClientConnected(ulong clientId)
        {
            //  Verify OfflineLobbyManager is spawned
            if (!IsSpawned)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                    " OfflineLobbyManager is NOT spawned on network! " +
                    "Make sure this GameObject has a NetworkObject component attached!");
                return;
            }

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $"Client {clientId} connected to lobby (IsServer: {IsServer}, IsClient: {IsClient})");

            if (networkManager.IsServer)
            {
                string playerName = _playerName;
                bool isHost = false;

                if (clientId == networkManager.LocalClientId)
                {
                    isHost = true;
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Host connected (local)");
                }
                else
                {
                    playerName = $"Player {clientId}"; // Temporary until we get real name
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Remote client {clientId} connected");
                }

                //Add to local list immediately (host only)
                AddPlayerToLobby(clientId, playerName, isHost, false);

                //  Delay NetworkList update to ensure client is ready
                StartCoroutine(AddPlayerToNetworkListDelayed(clientId, playerName, isHost));

                if (_currentLobbyData != null)
                {
                    _currentLobbyData.CurrentPlayers = GetRealPlayerCount();
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                        $"Updated lobby: {_currentLobbyData.CurrentPlayers} real players");
                }

                // Request player name from remote clients (with delay)
                if (clientId != networkManager.LocalClientId)
                {
                    StartCoroutine(RequestPlayerNameWithDelay(clientId));
                }

                // Fill with bots if enabled
                if (fillPlayerSpaces)
                {
                    FillRemainingSpacesWithBots();
                }
            }
            else if (IsClient && !IsServer)
            {
                // Client connected - send our player name to host after delay
                StartCoroutine(SendPlayerNameWithDelay());

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    "Client successfully connected and OfflineLobbyManager is synced");
            }
        }

        // Add delay to ensure network is fully initialized
        private IEnumerator RequestPlayerNameWithDelay(ulong clientId)
        {
            yield return new WaitForSeconds(0.2f);
            RequestPlayerNameClientRpc(clientId);
        }

        private IEnumerator SendPlayerNameWithDelay()
        {
            yield return new WaitForSeconds(0.2f);
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Client: Sending player name to host: {_playerName}");
            SendPlayerNameToHostServerRpc(networkManager.LocalClientId, _playerName);
        }

        [ClientRpc]
        private void RequestPlayerNameClientRpc(ulong targetClientId)
        {
            if (NetworkManager.Singleton.LocalClientId == targetClientId)
            {
                SendPlayerNameToHostServerRpc(targetClientId, _playerName);
            }
        }

        [ServerRpc(RequireOwnership = false)]
        private void SendPlayerNameToHostServerRpc(ulong clientId, string playerName)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" Server: Received player name from {clientId}: {playerName}");

            // Update local player list
            var player = _currentLobbyPlayers.Find(p => p.ClientId == clientId);
            if (player != null)
            {
                player.PlayerName = playerName;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    $" Updated local list for {clientId}: {playerName}");
            }

            //Update NetworkList
            for (int i = 0; i < _networkPlayers.Count; i++)
            {
                if (_networkPlayers[i].ClientId == clientId)
                {
                    var netPlayer = _networkPlayers[i];
                    netPlayer.PlayerName = playerName;
                    _networkPlayers[i] = netPlayer;
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                        $" Updated NetworkList for {clientId}: {playerName}");
                    break;
                }
            }

            // Notify all clients of the update
            NotifyPlayerNameUpdateClientRpc(clientId, playerName);
        }

        [ClientRpc]
        private void NotifyPlayerNameUpdateClientRpc(ulong clientId, string playerName)
        {
            if (IsServer) return; // Server already has this info

            var player = _currentLobbyPlayers.Find(p => p.ClientId == clientId);
            if (player != null)
            {
                player.PlayerName = playerName;
                OnPlayerJoined?.Invoke(player); // Trigger UI update
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Client: Updated player name for {clientId}: {playerName}");
            }
        }

        private void OnClientDisconnected(ulong clientId)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Client {clientId} disconnected from lobby");

            if (networkManager.IsServer)
            {
                // Remove from network list
                for (int i = _networkPlayers.Count - 1; i >= 0; i--)
                {
                    if (_networkPlayers[i].ClientId == clientId)
                    {
                        _networkPlayers.RemoveAt(i);
                        break;
                    }
                }

                RemovePlayerFromLobby(clientId);

                if (_currentLobbyData != null)
                {
                    _currentLobbyData.CurrentPlayers = GetRealPlayerCount();
                }

                // Notify all clients
                NotifyPlayerLeftClientRpc(clientId);
            }

            // Client disconnected from server
            if (networkManager.IsClient && !networkManager.IsServer && !networkManager.IsConnectedClient)
            {
                _currentLobbyData = null;
                _currentLobbyPlayers.Clear();
                if (_networkPlayers != null)
                {
                    _networkPlayers.Clear();
                }
                OnLobbyDisbanded?.Invoke();
            }
        }

        [ClientRpc]
        private void NotifyPlayerLeftClientRpc(ulong clientId)
        {
            if (IsServer) return; // Server already handled this

            RemovePlayerFromLobby(clientId);
        }
        private IEnumerator AddPlayerToNetworkListDelayed(ulong clientId, string playerName, bool isHost)
        {
            //  Wait for client to fully spawn and be ready (critical for sync)
            yield return new WaitForSeconds(0.5f);

            if (!IsServer || !IsSpawned) yield break;

            try
            {
                // Now it's safe to add to NetworkList
                NetworkPlayerData netPlayer = new NetworkPlayerData
                {
                    ClientId = clientId,
                    PlayerName = playerName,
                    IsReady = false,
                    IsHost = isHost
                };

                _networkPlayers.Add(netPlayer);

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    $" Added player to NetworkList: {playerName} (ID: {clientId})");

                //  Force sync to all clients using ClientRpc
                SyncPlayerListToAllClientsClientRpc();
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException,
                    $"Error adding player to NetworkList: {e.Message}", e);
            }
        }
        [ClientRpc]
        private void SyncPlayerListToAllClientsClientRpc()
        {
            if (IsServer) return; // Server already has the data

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $"📥 Receiving NetworkList sync - Count: {_networkPlayers.Count}");

            //  Clear client's local list and rebuild from NetworkList
            _currentLobbyPlayers.RemoveAll(p => !p.IsBot);

            //  Trigger manual sync from NetworkList
            foreach (var netPlayer in _networkPlayers)
            {
                PlayerInfo playerInfo = new PlayerInfo(
                    netPlayer.ClientId,
                    netPlayer.PlayerName.ToString(),
                    netPlayer.IsHost,
                    false
                );
                playerInfo.IsReady = netPlayer.IsReady;

                // Check if player already exists
                if (!_currentLobbyPlayers.Any(p => p.ClientId == netPlayer.ClientId))
                {
                    _currentLobbyPlayers.Add(playerInfo);
                    OnPlayerJoined?.Invoke(playerInfo);
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                        $" Client added player from sync: {playerInfo.PlayerName} (ID: {playerInfo.ClientId})");
                }
            }

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" Sync complete - Total players: {_currentLobbyPlayers.Count(p => !p.IsBot)} real");
        }
        #endregion

        #region Lobby Discovery
        private void StartDiscoveryServer()
        {
            try
            {
                _udpServer = new UdpClient();
                _udpServer.EnableBroadcast = true;
                _udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                _udpServer.Client.Bind(new IPEndPoint(IPAddress.Any, broadcastPort));

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Discovery server started");

                ListenForDiscoveryRequests();
                BroadcastPresence();
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Failed to start discovery server: {e.Message}", e);
                StopDiscoveryServer();
            }
        }

        private async void ListenForDiscoveryRequests()
        {
            while (_isHosting && _udpServer != null)
            {
                try
                {
                    UdpReceiveResult result = await _udpServer.ReceiveAsync();
                    string message = System.Text.Encoding.UTF8.GetString(result.Buffer);

                    if (message == "DISCOVER_LOBBY")
                    {
                        string lobbyInfo = JsonUtility.ToJson(_currentLobbyData);
                        byte[] responseData = System.Text.Encoding.UTF8.GetBytes(lobbyInfo);
                        await _udpServer.SendAsync(responseData, responseData.Length, result.RemoteEndPoint);
                    }
                }
                catch (Exception e)
                {
                    if (_isHosting)
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error in discovery server: {e.Message}", e);
                }
            }
        }

        private async void BroadcastPresence()
        {
            while (_isHosting && _udpServer != null)
            {
                try
                {
                    _currentLobbyData.LastDiscoveryTime = Time.time;
                    _currentLobbyData.TimeoutTime = Time.time + lobbyTimeout;

                    string lobbyInfo = JsonUtility.ToJson(_currentLobbyData);
                    byte[] data = System.Text.Encoding.UTF8.GetBytes(lobbyInfo);

                    List<IPAddress> broadcastAddresses = GetBroadcastAddresses();
                    foreach (var broadcastAddr in broadcastAddresses)
                    {
                        await _udpServer.SendAsync(data, data.Length, new IPEndPoint(broadcastAddr, broadcastPort));
                    }

                    await Task.Delay((int)(discoveryInterval * 1000));
                }
                catch (Exception e)
                {
                    if (_isHosting)
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error broadcasting presence: {e.Message}", e);
                }
            }
        }

        private void StopDiscoveryServer()
        {
            if (_udpServer != null)
            {
                _udpServer.Close();
                _udpServer.Dispose();
                _udpServer = null;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Discovery server stopped");
            }
        }

        private void StartDiscoveryClient()
        {
            try
            {
                _udpClient = new UdpClient();
                _udpClient.EnableBroadcast = true;
                _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0));

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Discovery client started");

                ListenForBroadcasts();
                SendDiscoveryRequest();
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Failed to start discovery client: {e.Message}", e);
                StopDiscoveryClient();
            }
        }

        private async void ListenForBroadcasts()
        {
            while (_isSearching && _udpClient != null)
            {
                try
                {
                    UdpReceiveResult result = await _udpClient.ReceiveAsync();
                    string message = System.Text.Encoding.UTF8.GetString(result.Buffer);

                    try
                    {
                        OfflineLobbyData lobbyData = JsonUtility.FromJson<OfflineLobbyData>(message);
                        if (lobbyData != null && !string.IsNullOrEmpty(lobbyData.HostAddress))
                        {
                            string lobbyKey = $"{lobbyData.HostAddress}:{lobbyData.Port}";
                            lobbyData.LastDiscoveryTime = Time.time;
                            lobbyData.TimeoutTime = Time.time + lobbyTimeout;

                            if (!_discoveredLobbies.ContainsKey(lobbyKey))
                            {
                                _discoveredLobbies.Add(lobbyKey, lobbyData);
                                OnLobbyDiscovered?.Invoke(lobbyData);
                                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Discovered lobby: {lobbyData.LobbyName} at {lobbyData.HostAddress}:{lobbyData.Port}");
                            }
                            else
                            {
                                _discoveredLobbies[lobbyKey] = lobbyData;
                            }
                        }
                    }
                    catch (Exception jsonEx)
                    {
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Received invalid lobby data: {jsonEx.Message}");
                    }
                }
                catch (Exception e)
                {
                    if (_isSearching && _udpClient != null)
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error in discovery client: {e.Message}", e);
                }
            }
        }

        private async void SendDiscoveryRequest()
        {
            while (_isSearching && _udpClient != null)
            {
                try
                {
                    byte[] data = System.Text.Encoding.UTF8.GetBytes("DISCOVER_LOBBY");

                    List<IPAddress> broadcastAddresses = GetBroadcastAddresses();
                    foreach (var broadcastAddr in broadcastAddresses)
                    {
                        await _udpClient.SendAsync(data, data.Length, new IPEndPoint(broadcastAddr, broadcastPort));
                    }

                    await Task.Delay((int)(discoveryInterval * 1000));
                }
                catch (Exception e)
                {
                    if (_isSearching)
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error sending discovery request: {e.Message}", e);
                }
            }
        }

        private void StopDiscoveryClient()
        {
            if (_udpClient != null)
            {
                _udpClient.Close();
                _udpClient.Dispose();
                _udpClient = null;
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Discovery client stopped");
            }
        }

        private void CleanupTimedOutLobbies()
        {
            if (!_isSearching)
                return;

            List<string> lobbiesForRemoval = new List<string>();
            float currentTime = Time.time;

            foreach (var kvp in _discoveredLobbies)
            {
                if (kvp.Value.TimeoutTime > 0 && currentTime > kvp.Value.TimeoutTime)
                {
                    lobbiesForRemoval.Add(kvp.Key);
                }
            }

            foreach (var lobbyKey in lobbiesForRemoval)
            {
                OnLobbyRemoved?.Invoke(lobbyKey);
                _discoveredLobbies.Remove(lobbyKey);
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Removed timed-out lobby: {lobbyKey}");
            }
        }

        private List<IPAddress> GetBroadcastAddresses()
        {
            List<IPAddress> addresses = new List<IPAddress>();
            addresses.Add(IPAddress.Broadcast);

            try
            {
                var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
                foreach (var netInterface in networkInterfaces)
                {
                    if (netInterface.OperationalStatus == OperationalStatus.Up &&
                        (netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
                         netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet))
                    {
                        var props = netInterface.GetIPProperties();
                        foreach (var addr in props.UnicastAddresses)
                        {
                            if (addr.Address.AddressFamily == AddressFamily.InterNetwork &&
                                !IPAddress.IsLoopback(addr.Address))
                            {
                                byte[] ipBytes = addr.Address.GetAddressBytes();
                                byte[] maskBytes = addr.IPv4Mask.GetAddressBytes();
                                byte[] broadcastBytes = new byte[4];

                                for (int i = 0; i < 4; i++)
                                {
                                    broadcastBytes[i] = (byte)(ipBytes[i] | ~maskBytes[i]);
                                }

                                addresses.Add(new IPAddress(broadcastBytes));
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error getting broadcast addresses: {ex.Message}", ex);
            }

            return addresses;
        }
        #endregion

        #region Lobby Management
        public async void StartHostingLobby(string lobbyName = "", int maxPlayers = 0, OfflineGameMode gameMode = OfflineGameMode.Random, OfflineGameMap gameMap = OfflineGameMap.Random)
        {
            if (!_isInitialized || _isShuttingDown)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, "OfflineLobbyManager not initialized or shutting down");
                OnHostLobbyResult?.Invoke(false);
                return;
            }

            // Ensure clean state before starting
            if (_isHosting || networkManager.IsListening)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Cleaning up existing lobby before hosting new one");
                await CleanupNetworkStateAsync();
            }

            _currentLobbyData = new OfflineLobbyData
            {
                LobbyName = !string.IsNullOrEmpty(lobbyName) ? lobbyName : defaultLobbyName,
                HostName = _playerName,
                HostAddress = GetLocalIPAddress(),
                Port = serverPort,
                CurrentPlayers = 1,
                MaxPlayers = maxPlayers > 0 ? maxPlayers : defaultMaxPlayers,
                GameMode = GetGameModeName(gameMode != OfflineGameMode.Random ? gameMode : defaultGameMode),
                GameModeEnum = gameMode,
                GameMapEnum = gameMap,
                LastDiscoveryTime = Time.time,
                TimeoutTime = Time.time + lobbyTimeout
            };

            _selectedGameMode = gameMode;
            _selectedGameMap = gameMap;

            try
            {
                if (!ValidateNetworkManagerConfiguration())
                {
                    throw new Exception("NetworkManager configuration validation failed");
                }

                unityTransport.ConnectionData.Address = _currentLobbyData.HostAddress;
                unityTransport.ConnectionData.Port = (ushort)serverPort;

                _isHosting = true;

                bool startResult = networkManager.StartHost();
                if (!startResult)
                {
                    throw new Exception("Failed to start NetworkManager as host");
                }

                // Wait for NetworkManager to initialize
                await Task.Delay(100);

                StartDiscoveryServer();

                OnHostLobbyResult?.Invoke(true);
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Successfully started hosting lobby: {_currentLobbyData.LobbyName}");
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Failed to start hosting lobby: {e.Message}", e);
                _isHosting = false;
                _currentLobbyData = null;
                OnHostLobbyResult?.Invoke(false);
            }
        }

        public void StopHostingLobby()
        {
            if (_isHosting)
            {
                _isHosting = false;
                StopDiscoveryServer();

                StartCoroutine(StopHostingLobbyCoroutine());
            }
        }

        private IEnumerator StopHostingLobbyCoroutine()
        {
            _currentLobbyData = null;
            _currentLobbyPlayers.Clear();
            if (_networkPlayers != null)
            {
                _networkPlayers.Clear();
            }

            if (networkManager != null && (networkManager.IsHost || networkManager.IsServer))
            {
                if (networkManager.ConnectedClientsIds != null && networkManager.ConnectedClientsIds.Count > 0)
                {
                    var clientIds = networkManager.ConnectedClientsIds.ToList();
                    foreach (var clientId in clientIds)
                    {
                        if (clientId != networkManager.LocalClientId)
                        {
                            networkManager.DisconnectClient(clientId);
                        }
                    }
                }

                yield return new WaitForSeconds(0.2f);

                networkManager.Shutdown();

                yield return new WaitForSeconds(0.3f);
            }

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Host lobby stopped and resources cleaned up");
        }

        public async void JoinLobby(OfflineLobbyData lobbyData)
        {
            if (!_isInitialized || _isShuttingDown)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                    "OfflineLobbyManager not initialized or shutting down");
                OnJoinLobbyResult?.Invoke(false);
                return;
            }

            // Ensure clean state before joining
            if (networkManager.IsListening)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    "Cleaning up existing connection before joining");
                await CleanupNetworkStateAsync();
            }

            try
            {
                unityTransport.ConnectionData.Address = lobbyData.HostAddress;
                unityTransport.ConnectionData.Port = (ushort)lobbyData.Port;

                //  Clear local list only
                _currentLobbyPlayers.Clear();

                _currentLobbyData = lobbyData;

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    $"Starting client to join address is {lobbyData.HostAddress} while port is :{lobbyData.Port}");

                bool startResult = networkManager.StartClient();
                if (!startResult)
                {
                    throw new Exception("Failed to start NetworkManager as client");
                }

                //  Wait for connection AND for NetworkObjects to spawn
                int attempts = 0;
                const int maxAttempts = 100; // 10 seconds total
                while (attempts < maxAttempts)
                {
                    await Task.Delay(100);
                    attempts++;

                    // Check if we're connected AND if OfflineLobbyManager is spawned
                    if (networkManager.IsConnectedClient && IsSpawned)
                    {
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                            $" Successfully connected and spawned (attempt {attempts})");
                        OnJoinLobbyResult?.Invoke(true);
                        return;
                    }

                    if (attempts % 10 == 0)
                    {
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                            $"Waiting for connection... (attempt {attempts}/{maxAttempts}, " +
                            $"IsConnected: {networkManager.IsConnectedClient}, IsSpawned: {IsSpawned})");
                    }
                }

                // Timeout - connection failed
                throw new Exception($"Connection timeout after {maxAttempts * 100}ms. " +
                    $"IsConnected: {networkManager.IsConnectedClient}, IsSpawned: {IsSpawned}");
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException,
                    $"Failed to join lobby: {e.Message}", e);
                _currentLobbyData = null;
                OnJoinLobbyResult?.Invoke(false);
            }
        }

        public void LeaveLobby()
        {
            StartCoroutine(LeaveLobbyCoroutine());
        }

        private IEnumerator LeaveLobbyCoroutine()
        {
            _currentLobbyData = null;
            _currentLobbyPlayers.Clear();
            if (_networkPlayers != null)
            {
                _networkPlayers.Clear();
            }

            if (networkManager != null && networkManager.IsConnectedClient)
            {
                networkManager.Shutdown();

                yield return new WaitForSeconds(0.5f);

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Left lobby and cleaned up resources");
            }
        }

        private async Task CleanupNetworkStateAsync()
        {
            if (networkManager == null || !networkManager.IsListening)
                return;

            try
            {
                if (networkManager.IsServer && networkManager.ConnectedClientsIds != null)
                {
                    var clientIds = new List<ulong>(networkManager.ConnectedClientsIds);
                    foreach (var clientId in clientIds)
                    {
                        if (clientId != networkManager.LocalClientId)
                        {
                            networkManager.DisconnectClient(clientId);
                        }
                    }
                }

                await Task.Delay(200);

                networkManager.Shutdown();

                await Task.Delay(500);

                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Network state cleaned up successfully");
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error during network cleanup: {e.Message}", e);
            }
        }

        public void StartSearchingForLobbies()
        {
            if (!_isInitialized || _isSearching)
                return;

            _isSearching = true;
            _discoveredLobbies.Clear();
            StartDiscoveryClient();

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Started searching for lobbies");
        }

        public void StopSearchingForLobbies()
        {
            if (!_isSearching)
                return;

            _isSearching = false;
            StopDiscoveryClient();
            _discoveredLobbies.Clear();

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Stopped searching for lobbies");
        }
        #endregion

        #region Player Management
        private void AddPlayerToLobby(ulong clientId, string playerName, bool isHost, bool isBot)
        {
            PlayerInfo playerInfo = new PlayerInfo(clientId, playerName, isHost, isBot);
            _currentLobbyPlayers.Add(playerInfo);
            OnPlayerJoined?.Invoke(playerInfo);
        }

        private void RemovePlayerFromLobby(ulong clientId)
        {
            PlayerInfo playerToRemove = _currentLobbyPlayers.Find(p => p.ClientId == clientId);
            if (playerToRemove != null)
            {
                _currentLobbyPlayers.Remove(playerToRemove);
                OnPlayerLeft?.Invoke(clientId);
            }
        }

        private int GetRealPlayerCount()
        {
            return _currentLobbyPlayers.Count(p => !p.IsBot);
        }

        private string GenerateBotName()
        {
            string prefix = botNamePrefixes[Random.Range(0, botNamePrefixes.Length)];
            string suffix = botNameSuffixes[Random.Range(0, botNameSuffixes.Length)];
            return $"{prefix}{suffix}";
        }

        private ulong GenerateBotId()
        {
            ulong botId = 10000;
            while (_currentLobbyPlayers.Any(p => p.ClientId == botId))
            {
                botId++;
            }
            return botId;
        }

        public void SetPlayerName(string playerName)
        {
            if (!string.IsNullOrEmpty(playerName))
            {
                _playerName = playerName;
                PlayerPrefs.SetString("PlayerName", _playerName);
                PlayerPrefs.Save();
            }
        }

        public void SetPlayerReady(ulong clientId, bool isReady)
        {
            //   Check if OfflineLobbyManager is spawned
            if (!IsSpawned)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                    " Cannot set player ready: OfflineLobbyManager is NOT spawned on network!\n" +
                    "SOLUTION: Make sure the GameObject with OfflineLobbyManager has a NetworkObject component!");
                return;
            }

            //  Check if client is connected
            if (!networkManager.IsConnectedClient)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                    " Cannot set player ready: Not connected to network");
                return;
            }

            //  Update local list immediately
            PlayerInfo player = _currentLobbyPlayers.Find(p => p.ClientId == clientId);
            if (player != null)
            {
                player.IsReady = isReady;
                OnPlayerReadyStatusChanged?.Invoke(player);
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    $"Local: Player {clientId} ready status: {isReady}");
            }

            //  Update NetworkList if server
            if (IsServer)
            {
                for (int i = 0; i < _networkPlayers.Count; i++)
                {
                    if (_networkPlayers[i].ClientId == clientId)
                    {
                        var netPlayer = _networkPlayers[i];
                        netPlayer.IsReady = isReady;
                        _networkPlayers[i] = netPlayer;
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                            $" NetworkList: Player {clientId} ready status updated: {isReady}");
                        break;
                    }
                }

                //  Force sync to all clients
                NotifyPlayerReadyStatusClientRpc(clientId, isReady);
            }
            else
            {
                //  Client: Request server to update
                try
                {
                    SetPlayerReadyServerRpc(clientId, isReady);
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                        $" Sent SetPlayerReady ServerRpc for client {clientId}");
                }
                catch (System.Exception e)
                {
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException,
                        $" Failed to send SetPlayerReady ServerRpc: {e.Message}", e);
                }
            }
        }
        [ServerRpc(RequireOwnership = false)]
        private void SetPlayerReadyServerRpc(ulong clientId, bool isReady)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" Server: Received ready status from {clientId}: {isReady}");

            // Update NetworkList
            for (int i = 0; i < _networkPlayers.Count; i++)
            {
                if (_networkPlayers[i].ClientId == clientId)
                {
                    var netPlayer = _networkPlayers[i];
                    netPlayer.IsReady = isReady;
                    _networkPlayers[i] = netPlayer;
                    break;
                }
            }

            // Update local list
            var player = _currentLobbyPlayers.Find(p => p.ClientId == clientId);
            if (player != null)
            {
                player.IsReady = isReady;
                OnPlayerReadyStatusChanged?.Invoke(player);
            }

            // Notify all clients
            NotifyPlayerReadyStatusClientRpc(clientId, isReady);
        }
        [ClientRpc]
        private void NotifyPlayerReadyStatusClientRpc(ulong clientId, bool isReady)
        {
            if (IsServer) return; // Server already handled this

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                $" Client: Received ready status for {clientId}: {isReady}");

            var player = _currentLobbyPlayers.Find(p => p.ClientId == clientId);
            if (player != null)
            {
                player.IsReady = isReady;
                OnPlayerReadyStatusChanged?.Invoke(player);
            }
        }
        public bool AreAllPlayersReady()
        {
            if (_currentLobbyPlayers.Count == 0)
                return false;

            var realPlayers = _currentLobbyPlayers.Where(p => !p.IsBot).ToList();
            if (realPlayers.Count == 0)
                return false;

            return realPlayers.All(p => p.IsReady);
        }
        #endregion

        #region Bot Management
        public void SetFillPlayerSpaces(bool enabled)
        {
            fillPlayerSpaces = enabled;
            if (_isHosting)
            {
                if (enabled)
                {
                    FillRemainingSpacesWithBots();
                }
                else
                {
                    RemoveAllBots();
                }
            }
        }

        public bool GetFillPlayerSpaces()
        {
            return fillPlayerSpaces;
        }

        private void RemoveAllBots()
        {
            var botsToRemove = _currentLobbyPlayers.Where(p => p.IsBot).ToList();
            foreach (var bot in botsToRemove)
            {
                RemovePlayerFromLobby(bot.ClientId);
            }
        }

        public int CalculateBotsNeeded()
        {
            if (!fillPlayerSpaces || _currentLobbyData == null) return 0;

            int realPlayerCount = GetRealPlayerCount();
            int totalPlayersNeeded = _currentLobbyData.MaxPlayers;

            if (_selectedGameMode == OfflineGameMode.TeamDeathMatch)
            {
                int botsNeeded = totalPlayersNeeded - realPlayerCount;

                if ((realPlayerCount + botsNeeded) % 2 != 0 && botsNeeded > 0)
                {
                    botsNeeded--;
                }

                return Math.Min(botsNeeded, maxBots);
            }

            return Math.Min(totalPlayersNeeded - realPlayerCount, maxBots);
        }

        private void FillRemainingSpacesWithBots()
        {
            if (!_isHosting || _currentLobbyData == null || !fillPlayerSpaces) return;

            int botsNeeded = CalculateBotsNeeded();
            int currentBots = _currentLobbyPlayers.Count(p => p.IsBot);

            if (currentBots > botsNeeded)
            {
                var botsToRemove = _currentLobbyPlayers.Where(p => p.IsBot).Take(currentBots - botsNeeded).ToList();
                foreach (var bot in botsToRemove)
                {
                    RemovePlayerFromLobby(bot.ClientId);
                }
            }
            else if (botsNeeded > currentBots)
            {
                int botsToAdd = botsNeeded - currentBots;
                for (int i = 0; i < botsToAdd; i++)
                {
                    ulong botId = GenerateBotId();
                    string botName = GenerateBotName();
                    PlayerInfo botInfo = new PlayerInfo(botId, botName, false, true);
                    botInfo.IsReady = Random.value > 0.3f;

                    _currentLobbyPlayers.Add(botInfo);
                    OnPlayerJoined?.Invoke(botInfo);
                    OnPlayerReadyStatusChanged?.Invoke(botInfo);
                }
            }
        }
        #endregion

        #region Game Start
        public void StartGame(AllLoadableScenes gameplayScene)
        {
            if (!_isHosting)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, "Only the host can start the game");
                return;
            }

            if (!AreAllPlayersReady())
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, "Cannot start game: Not all players are ready");
                return;
            }

            try
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                    $"Starting offline game - transitioning to: {gameplayScene}");

                //   Disable lobby components BEFORE loading gameplay scene
                if (MidManStudio.NetworkScene.LobbySceneComponentManager.Instance != null)
                {
                    MidManStudio.NetworkScene.LobbySceneComponentManager.Instance.DisableLobbyComponents();
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Disabled lobby components for gameplay");
                }

                //  Use NetworkSceneManager to load ADDITIVELY for all clients
                if (networkManager != null && networkManager.SceneManager != null)
                {
                    // Convert AllLoadableScenes enum to scene name string
                    string sceneName = gameplayScene.ToString();

                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                        $"Loading scene ADDITIVELY for all clients: {sceneName}");
                    // In OfflineLobbyManager when starting game:
                    MID_TheSceneManager.Instance.LoadScene(AllLoadableScenes.OfflineMultiplayerScene, useTransition: true);
                    /* Load scene ADDITIVELY - lobby scene stays loaded the other method single be inconsistent
                    var status = networkManager.SceneManager.LoadScene(
                        sceneName,
                        UnityEngine.SceneManagement.LoadSceneMode.Additive  //  ADDITIVE!
                    );

                    if (status != Unity.Netcode.SceneEventProgressStatus.Started)
                    {
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                            $"Failed to start scene load: {status}");

                        // Re-enable lobby components if load failed
                        if (MidManStudio.NetworkScene.LobbySceneComponentManager.Instance != null)
                        {
                            MidManStudio.NetworkScene.LobbySceneComponentManager.Instance.EnableLobbyComponents();
                        }
                    }
                    else
                    {
                        MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log,
                            " Scene load started for all clients (ADDITIVE mode)");
                    }
                    */
                }
                else
                {
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError,
                        "NetworkManager or SceneManager is null");
                }
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException,
                    $"Failed to start game: {e.Message}", e);

                // Re-enable lobby components on error
                if (LobbySceneComponentManager.Instance != null)
                {
                   LobbySceneComponentManager.Instance.EnableLobbyComponents();
                }
            }
        }
        #endregion

        #region Mobile Network Support
        //find better code snippets on the interwebs the below does not work
        public void PromptForHotspot()
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Prompting user to enable hotspot");

#if UNITY_ANDROID && !UNITY_EDITOR
            try
            {
                using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
                using (var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity"))
                using (var intent = new AndroidJavaObject("android.content.Intent", "android.settings.WIRELESS_SETTINGS"))
                {
                    currentActivity.Call("startActivity", intent);
                }
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, $"Failed to open hotspot settings: {e.Message}");
            }
#elif UNITY_IOS && !UNITY_EDITOR
            Application.OpenURL("App-Prefs:root=WIFI");
#else
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, "Hotspot prompt not available on this platform");
#endif
        }

        public void PromptForWiFi()
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Prompting user to connect to WiFi");

#if UNITY_ANDROID && !UNITY_EDITOR
            try
            {
                using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
                using (var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity"))
                using (var intent = new AndroidJavaObject("android.content.Intent", "android.settings.WIFI_SETTINGS"))
                {
                    currentActivity.Call("startActivity", intent);
                }
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, $"Failed to open WiFi settings: {e.Message}");
            }
#elif UNITY_IOS && !UNITY_EDITOR
            Application.OpenURL("App-Prefs:root=WIFI");
#else
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, "WiFi prompt not available on this platform");
#endif
        }
        #endregion

        #region Utility Methods
        private string GetLocalIPAddress()
        {
            try
            {
                var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
                foreach (var netInterface in networkInterfaces)
                {
                    if (netInterface.OperationalStatus == OperationalStatus.Up &&
                        (netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
                         netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet))
                    {
                        var props = netInterface.GetIPProperties();
                        foreach (var addr in props.UnicastAddresses)
                        {
                            if (addr.Address.AddressFamily == AddressFamily.InterNetwork &&
                                !IPAddress.IsLoopback(addr.Address))
                            {
                                return addr.Address.ToString();
                            }
                        }
                    }
                }

                // Check for hotspot IP
                foreach (var netInterface in networkInterfaces)
                {
                    var props = netInterface.GetIPProperties();
                    foreach (var addr in props.UnicastAddresses)
                    {
                        if (addr.Address.AddressFamily == AddressFamily.InterNetwork)
                        {
                            string ip = addr.Address.ToString();
                            if (ip.StartsWith("192.168.43.") || ip.StartsWith("172.20.10."))
                            {
                                return ip;
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error getting local IP address: {ex.Message}", ex);
            }
            return "127.0.0.1";
        }

        private void InitializeGameModes()
        {
            _gameModeMaxPlayers.Clear();
            _gameModeMaxPlayers.Add(OfflineGameMode.Random, 8);
            _gameModeMaxPlayers.Add(OfflineGameMode.TeamDeathMatch, 10);
            _gameModeMaxPlayers.Add(OfflineGameMode.HardPoint, 10);
            _gameModeMaxPlayers.Add(OfflineGameMode.Objectives, 8);
            _gameModeMaxPlayers.Add(OfflineGameMode.IgniteDelivery, 6);
            _gameModeMaxPlayers.Add(OfflineGameMode.Survival, 4);
        }

        public string GetGameModeName(OfflineGameMode gameMode)
        {
            switch (gameMode)
            {
                case OfflineGameMode.Random: return "Random";
                case OfflineGameMode.TeamDeathMatch: return "Team Death Match";
                case OfflineGameMode.HardPoint: return "Hard Point";
                case OfflineGameMode.Objectives: return "Objectives";
                case OfflineGameMode.IgniteDelivery: return "Ignite Delivery";
                case OfflineGameMode.Survival: return "Survival";
                default: return "Unknown";
            }
        }

        public int GetMaxPlayersForGameMode(OfflineGameMode gameMode)
        {
            if (_gameModeMaxPlayers.TryGetValue(gameMode, out int maxPlayers))
            {
                return maxPlayers;
            }
            return defaultMaxPlayers;
        }

        public string GetGameMapName(OfflineGameMap gameMap)
        {
            switch (gameMap)
            {
                case OfflineGameMap.Random: return "Random";
                case OfflineGameMap.GrassyLand: return "Grassy Land";
                case OfflineGameMap.TakiLand: return "Taki Land";
                case OfflineGameMap.CrystalCavern: return "Crystal Cavern";
                case OfflineGameMap.TheFall: return "The Fall";
                case OfflineGameMap.BlackRock: return "Black Rock";
                case OfflineGameMap.Base: return "Base Map";
                default: return "Unknown";
            }
        }

        public void SetSelectedGameMode(OfflineGameMode gameMode)
        {
            _selectedGameMode = gameMode;
        }

        public void SetSelectedGameMap(OfflineGameMap gameMap)
        {
            _selectedGameMap = gameMap;
        }

        public OfflineGameMode GetSelectedGameMode()
        {
            return _selectedGameMode;
        }

        public OfflineGameMap GetSelectedGameMap()
        {
            return _selectedGameMap;
        }

        public new bool IsHost() => networkManager != null && networkManager.IsHost;
        public bool IsInLobby() => networkManager != null && (networkManager.IsConnectedClient || networkManager.IsHost);
        public List<PlayerInfo> GetCurrentPlayers() => new List<PlayerInfo>(_currentLobbyPlayers);
        public Dictionary<string, OfflineLobbyData> GetDiscoveredLobbies() => new Dictionary<string, OfflineLobbyData>(_discoveredLobbies);
        public string GetPlayerName() => _playerName;
        public int GetMaxBots() => maxBots;
        public bool ShouldFillWithBots() => fillPlayerSpaces;
        #endregion
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using MidManStudio.Core.HelperFunctions;
using MidManStudio.OfflineLobby;

public class MobileNetworkDiscovery : MonoBehaviour
{
    [Header("Network Configuration")]
    [SerializeField] private int broadcastPort = 7778;
    [SerializeField] private float discoveryInterval = 2.0f;
    [SerializeField] private float lobbyTimeout = 30f;
    [SerializeField] private int maxRetries = 3;
    [SerializeField] private float retryDelay = 1f;

    [Header("Mobile Optimizations")]
    [SerializeField] private bool useMulticastForMobile = true;
    [SerializeField] private string multicastAddress = "239.255.255.250";
    [SerializeField] private bool enableFallbackDiscovery = true;
    [SerializeField] private int[] fallbackPorts = { 7779, 7780, 7781 };

    private bool _isServer = false;
    private bool _isClient = false;
    private UdpClient _udpServer;
    private UdpClient _udpClient;
    private UdpClient _multicastClient;
    private Dictionary<string, OfflineLobbyData> _discoveredLobbies = new Dictionary<string, OfflineLobbyData>();
    private OfflineLobbyData _serverData;
    private Coroutine _serverBroadcastCoroutine;
    private Coroutine _clientDiscoveryCoroutine;
    private Coroutine _cleanupCoroutine;

    // Events
    public Action<OfflineLobbyData> OnLobbyDiscovered;
    public Action<string> OnLobbyRemoved;

    #region Unity Lifecycle
    private void Start()
    {
        _cleanupCoroutine = StartCoroutine(CleanupTimedOutLobbies());
    }

    private void OnDestroy()
    {
        StopAllCoroutines();
        StopServer();
        StopClient();
    }

    private void OnApplicationPause(bool pauseStatus)
    {
        if (Application.isMobilePlatform)
        {
            if (pauseStatus)
            {
                // App is being paused - stop network operations
                StopNetworkOperations();
            }
            else
            {
                // App is being resumed - restart network operations if needed
                RestartNetworkOperations();
            }
        }
    }
    #endregion

    #region Public Methods
    public void StartServer(OfflineLobbyData serverData)
    {
        if (_isServer)
        {
            StopServer();
        }

        _serverData = serverData;
        _serverData.LastDiscoveryTime = Time.time;
        _serverData.TimeoutTime = Time.time + lobbyTimeout;

        if (string.IsNullOrEmpty(_serverData.HostAddress))
        {
            _serverData.HostAddress = GetLocalIPAddress();
        }

        _isServer = true;

        StartCoroutine(StartServerAsync());
    }

    public void StopServer()
    {
        _isServer = false;

        if (_serverBroadcastCoroutine != null)
        {
            StopCoroutine(_serverBroadcastCoroutine);
            _serverBroadcastCoroutine = null;
        }

        StopServerComponents();
        _serverData = default;
    }

    public void StartClient()
    {
        if (_isClient)
        {
            StopClient();
        }

        _isClient = true;
        _discoveredLobbies.Clear();

        StartCoroutine(StartClientAsync());
    }

    public void StopClient()
    {
        _isClient = false;

        if (_clientDiscoveryCoroutine != null)
        {
            StopCoroutine(_clientDiscoveryCoroutine);
            _clientDiscoveryCoroutine = null;
        }

        StopClientComponents();
        _discoveredLobbies.Clear();
    }

    public Dictionary<string, OfflineLobbyData> GetDiscoveredLobbies()
    {
        return new Dictionary<string, OfflineLobbyData>(_discoveredLobbies);
    }

    public bool IsServerRunning() => _isServer;
    public bool IsClientRunning() => _isClient;
    #endregion

    #region Server Implementation
    private IEnumerator StartServerAsync()
    {
        int attempts = 0;
        bool success = false;
        
        while (attempts < maxRetries && !success && _isServer)
        {
            attempts++;

            // Try standard UDP broadcast first
            success = CreateUdpServer(broadcastPort + attempts - 1);

            if (!success && enableFallbackDiscovery)
            {
                // Try fallback ports
                foreach (int port in fallbackPorts)
                {
                    if (CreateUdpServer(port))
                    {
                        success = true;
                        break;
                    }
                }
            }

            if (!success && useMulticastForMobile && Application.isMobilePlatform)
            {
                // Try multicast as last resort
                success = CreateMulticastServer();
            }

            if (success)
            {
                _serverBroadcastCoroutine = StartCoroutine(ServerBroadcastLoop());
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Network discovery server started successfully");
            }
            else
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Server start attempt {attempts} failed");
                yield return new WaitForSeconds(retryDelay);
            }
        }

        if (!success)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, "Failed to start network discovery server after all attempts");
            _isServer = false;
        }
    }

    private bool CreateUdpServer(int port)
    {
        try
        {
            _udpServer?.Close();
            _udpServer?.Dispose();

            _udpServer = new UdpClient();
            _udpServer.EnableBroadcast = true;

            if (Application.isMobilePlatform)
            {
                _udpServer.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            }

            _udpServer.Client.Bind(new IPEndPoint(IPAddress.Any, port));
            broadcastPort = port; // Update the actual port being used

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"UDP server created on port {port}");
            return true;
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Failed to create UDP server on port {port}: {e.Message}");
            return false;
        }
    }

    private bool CreateMulticastServer()
    {
        try
        {
            _multicastClient?.Close();
            _multicastClient?.Dispose();

            _multicastClient = new UdpClient();
            _multicastClient.JoinMulticastGroup(IPAddress.Parse(multicastAddress));

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Multicast server created on {multicastAddress}");
            return true;
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Failed to create multicast server: {e.Message}");
            return false;
        }
    }

    private IEnumerator ServerBroadcastLoop()
    {
        while (_isServer && (_udpServer != null || _multicastClient != null))
        {
            // Update server data
            _serverData.LastDiscoveryTime = Time.time;
            _serverData.TimeoutTime = Time.time + lobbyTimeout;

            string jsonData = JsonUtility.ToJson(_serverData);
            byte[] data = Encoding.UTF8.GetBytes("LOBBY_BROADCAST:" + jsonData);

            // Broadcast via UDP
            if (_udpServer != null)
            {
                StartCoroutine(BroadcastToNetwork(data));
            }

            // Broadcast via Multicast (mobile fallback)
            if (_multicastClient != null)
            {
                StartCoroutine(MulticastToNetwork(data));
            }

            // Listen for discovery requests
            StartCoroutine(ListenForDiscoveryRequests());

            yield return new WaitForSeconds(discoveryInterval);
        }
    }

    private IEnumerator BroadcastToNetwork(byte[] data)
    {
        List<IPEndPoint> broadcastEndpoints = GetBroadcastEndpoints();

        foreach (var endpoint in broadcastEndpoints)
        {
            StartCoroutine(SendDataToEndpoint(_udpServer, data, endpoint));
            yield return null; // Yield to prevent blocking
        }
    }

    private IEnumerator MulticastToNetwork(byte[] data)
    {
        IPEndPoint multicastEndpoint = new IPEndPoint(IPAddress.Parse(multicastAddress), broadcastPort);
        StartCoroutine(SendDataToEndpoint(_multicastClient, data, multicastEndpoint));
        yield return null;
    }

    private IEnumerator SendDataToEndpoint(UdpClient client, byte[] data, IPEndPoint endpoint)
    {
        if (client == null) yield break;

        Task sendTask = null;

        try
        {
            sendTask = client.SendAsync(data, data.Length, endpoint);
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Send failed to {endpoint}: {e.Message}");
            yield break;
        }

        // Wait for completion with timeout
        float timeout = 2f;
        float elapsed = 0f;

        while (!sendTask.IsCompleted && elapsed < timeout)
        {
            elapsed += Time.deltaTime;
            yield return null;
        }

        if (!sendTask.IsCompleted)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Send timeout to {endpoint}");
        }
        else if (sendTask.IsFaulted)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Send task faulted to {endpoint}: {sendTask.Exception?.Message}");
        }
    }

    private IEnumerator ListenForDiscoveryRequests()
    {
        if (_udpServer == null) yield break;

        Task<UdpReceiveResult> receiveTask = null;

        try
        {
            receiveTask = _udpServer.ReceiveAsync();
        }
        catch (Exception)
        {
            yield break;
        }

        // Quick non-blocking check
        float timeout = 0.1f;
        float elapsed = 0f;

        while (!receiveTask.IsCompleted && elapsed < timeout)
        {
            elapsed += Time.deltaTime;
            yield return null;
        }

        if (receiveTask.IsCompleted && !receiveTask.IsFaulted)
        {
            try
            {
                var result = receiveTask.Result;
                string message = Encoding.UTF8.GetString(result.Buffer);

                if (message.StartsWith("DISCOVERY_REQUEST"))
                {
                    // Respond with our lobby data
                    string response = "DISCOVERY_RESPONSE:" + JsonUtility.ToJson(_serverData);
                    byte[] responseData = Encoding.UTF8.GetBytes(response);

                    StartCoroutine(SendDataToEndpoint(_udpServer, responseData, result.RemoteEndPoint));
                }
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Error processing discovery request: {e.Message}");
            }
        }
    }

    private void StopServerComponents()
    {
        try
        {
            _udpServer?.Close();
            _udpServer?.Dispose();
            _udpServer = null;

            _multicastClient?.Close();
            _multicastClient?.Dispose();
            _multicastClient = null;

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Server components stopped");
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error stopping server components: {e.Message}", e);
        }
    }
    #endregion

    #region Client Implementation
    private IEnumerator StartClientAsync()
    {
        int attempts = 0;
        bool success = false;

        while (attempts < maxRetries && !success && _isClient)
        {
            attempts++;

            success = CreateUdpClient();

            if (!success && useMulticastForMobile && Application.isMobilePlatform)
            {
                success = CreateMulticastClient();
            }

            if (success)
            {
                _clientDiscoveryCoroutine = StartCoroutine(ClientDiscoveryLoop());
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Network discovery client started successfully");
            }
            else
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Client start attempt {attempts} failed");
                yield return new WaitForSeconds(retryDelay);
            }
        }

        if (!success)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogError, "Failed to start network discovery client after all attempts");
            _isClient = false;
        }
    }

    private bool CreateUdpClient()
    {
        try
        {
            _udpClient?.Close();
            _udpClient?.Dispose();

            _udpClient = new UdpClient();
            _udpClient.EnableBroadcast = true;

            if (Application.isMobilePlatform)
            {
                _udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            }

            _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0));

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "UDP client created");
            return true;
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Failed to create UDP client: {e.Message}");
            return false;
        }
    }

    private bool CreateMulticastClient()
    {
        try
        {
            _multicastClient?.Close();
            _multicastClient?.Dispose();

            _multicastClient = new UdpClient();
            _multicastClient.JoinMulticastGroup(IPAddress.Parse(multicastAddress));

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Multicast client created");
            return true;
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Failed to create multicast client: {e.Message}");
            return false;
        }
    }

    private IEnumerator ClientDiscoveryLoop()
    {
        while (_isClient && (_udpClient != null || _multicastClient != null))
        {
            // Send discovery requests
            StartCoroutine(SendDiscoveryRequests());

            // Listen for broadcasts and responses
            StartCoroutine(ListenForBroadcasts());

            yield return new WaitForSeconds(discoveryInterval);
        }
    }

    private IEnumerator SendDiscoveryRequests()
    {
        string request = "DISCOVERY_REQUEST";
        byte[] data = Encoding.UTF8.GetBytes(request);

        // Send via UDP broadcast
        if (_udpClient != null)
        {
            List<IPEndPoint> broadcastEndpoints = GetBroadcastEndpoints();
            foreach (var endpoint in broadcastEndpoints)
            {
                StartCoroutine(SendDataToEndpoint(_udpClient, data, endpoint));
                yield return null; // Yield to prevent blocking
            }
        }

        // Send via multicast
        if (_multicastClient != null)
        {
            IPEndPoint multicastEndpoint = new IPEndPoint(IPAddress.Parse(multicastAddress), broadcastPort);
            StartCoroutine(SendDataToEndpoint(_multicastClient, data, multicastEndpoint));
        }

        yield return null;
    }

    private IEnumerator ListenForBroadcasts()
    {
        // Listen on UDP client
        if (_udpClient != null)
        {
            StartCoroutine(ListenOnClient(_udpClient));
        }

        // Listen on multicast client
        if (_multicastClient != null)
        {
            StartCoroutine(ListenOnClient(_multicastClient));
        }

        yield return null;
    }

    private IEnumerator ListenOnClient(UdpClient client)
    {
        if (client == null) yield break;

        Task<UdpReceiveResult> receiveTask = null;

        try
        {
            receiveTask = client.ReceiveAsync();
        }
        catch (Exception)
        {
            yield break;
        }

        // Quick non-blocking check
        float timeout = 0.5f;
        float elapsed = 0f;

        while (!receiveTask.IsCompleted && elapsed < timeout)
        {
            elapsed += Time.deltaTime;
            yield return null;
        }

        if (receiveTask.IsCompleted && !receiveTask.IsFaulted)
        {
            try
            {
                var result = receiveTask.Result;
                string message = Encoding.UTF8.GetString(result.Buffer);

                ProcessReceivedMessage(message, result.RemoteEndPoint);
            }
            catch (Exception e)
            {
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Error processing received message: {e.Message}");
            }
        }
    }

    private void ProcessReceivedMessage(string message, IPEndPoint remoteEndPoint)
    {
        try
        {
            OfflineLobbyData lobbyData = null;

            if (message.StartsWith("LOBBY_BROADCAST:"))
            {
                string jsonData = message.Substring("LOBBY_BROADCAST:".Length);
                lobbyData = JsonUtility.FromJson<OfflineLobbyData>(jsonData);
            }
            else if (message.StartsWith("DISCOVERY_RESPONSE:"))
            {
                string jsonData = message.Substring("DISCOVERY_RESPONSE:".Length);
                lobbyData = JsonUtility.FromJson<OfflineLobbyData>(jsonData);
            }

            if (lobbyData != null)
            {
                lobbyData.HostAddress = remoteEndPoint.Address.ToString();
                lobbyData.LastDiscoveryTime = Time.time;
                lobbyData.TimeoutTime = Time.time + lobbyTimeout; // MAKE SURE THIS LINE EXISTS

                string lobbyKey = $"{lobbyData.HostAddress}:{lobbyData.Port}";

                if (!_discoveredLobbies.ContainsKey(lobbyKey))
                {
                    _discoveredLobbies.Add(lobbyKey, lobbyData);
                    OnLobbyDiscovered?.Invoke(lobbyData);
                    MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Discovered lobby: {lobbyData.LobbyName} at {lobbyData.HostAddress}");
                }
                else
                {
                    _discoveredLobbies[lobbyKey] = lobbyData;
                }
            }
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogWarning, $"Failed to process received message: {e.Message}");
        }
    }

    private void StopClientComponents()
    {
        try
        {
            _udpClient?.Close();
            _udpClient?.Dispose();
            _udpClient = null;

            _multicastClient?.Close();
            _multicastClient?.Dispose();
            _multicastClient = null;

            MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, "Client components stopped");
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error stopping client components: {e.Message}", e);
        }
    }
    #endregion

    #region Utility Methods
    private List<IPEndPoint> GetBroadcastEndpoints()
    {
        List<IPEndPoint> endpoints = new List<IPEndPoint>();

        try
        {
            // Add global broadcast
            endpoints.Add(new IPEndPoint(IPAddress.Broadcast, broadcastPort));

            // Add subnet-specific broadcasts
            var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
            foreach (var netInterface in networkInterfaces)
            {
                if (netInterface.OperationalStatus == OperationalStatus.Up &&
                    (netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
                     netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet))
                {
                    var properties = netInterface.GetIPProperties();
                    foreach (var ip in properties.UnicastAddresses)
                    {
                        if (ip.Address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip.Address))
                        {
                            // Calculate broadcast address
                            byte[] ipBytes = ip.Address.GetAddressBytes();
                            byte[] maskBytes = ip.IPv4Mask.GetAddressBytes();
                            byte[] broadcastBytes = new byte[4];

                            for (int i = 0; i < 4; i++)
                            {
                                broadcastBytes[i] = (byte)(ipBytes[i] | ~maskBytes[i]);
                            }

                            var broadcastAddress = new IPAddress(broadcastBytes);
                            endpoints.Add(new IPEndPoint(broadcastAddress, broadcastPort));
                        }
                    }
                }
            }

            // Add fallback ports if enabled
            if (enableFallbackDiscovery)
            {
                foreach (int port in fallbackPorts)
                {
                    endpoints.Add(new IPEndPoint(IPAddress.Broadcast, port));
                }
            }
        }
        catch (Exception e)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error getting broadcast endpoints: {e.Message}", e);
            // Fallback to basic broadcast
            endpoints.Clear();
            endpoints.Add(new IPEndPoint(IPAddress.Broadcast, broadcastPort));
        }

        return endpoints;
    }

    private string GetLocalIPAddress()
    {
        try
        {
            var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
            foreach (var netInterface in networkInterfaces)
            {
                if (netInterface.OperationalStatus == OperationalStatus.Up &&
                    (netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 ||
                     netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet))
                {
                    var props = netInterface.GetIPProperties();
                    foreach (var addr in props.UnicastAddresses)
                    {
                        if (addr.Address.AddressFamily == AddressFamily.InterNetwork &&
                            !IPAddress.IsLoopback(addr.Address))
                        {
                            return addr.Address.ToString();
                        }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            MID_HelperFunctions.DebugOnlyInEditor(DebugType.LogException, $"Error getting local IP: {ex.Message}", ex);
        }
        return "127.0.0.1";
    }

    private IEnumerator CleanupTimedOutLobbies()
    {
        while (true)
        {
            yield return new WaitForSeconds(5f); // Check every 5 seconds

            List<string> keysToRemove = new List<string>();
            float currentTime = Time.time;

            foreach (var kvp in _discoveredLobbies)
            {
                if (kvp.Value.TimeoutTime > 0 && currentTime > kvp.Value.TimeoutTime)
                {
                    keysToRemove.Add(kvp.Key);
                }
            }

            foreach (var key in keysToRemove)
            {
                _discoveredLobbies.Remove(key);
                OnLobbyRemoved?.Invoke(key);
                MID_HelperFunctions.DebugOnlyInEditor(DebugType.Log, $"Removed timed out lobby: {key}");
            }
        }
    }

    private void StopNetworkOperations()
    {
        // Temporarily stop network operations when app is paused
        if (_serverBroadcastCoroutine != null)
        {
            StopCoroutine(_serverBroadcastCoroutine);
            _serverBroadcastCoroutine = null;
        }

        if (_clientDiscoveryCoroutine != null)
        {
            StopCoroutine(_clientDiscoveryCoroutine);
            _clientDiscoveryCoroutine = null;
        }
    }

    private void RestartNetworkOperations()
    {
        // Restart network operations when app is resumed
        if (_isServer && _serverBroadcastCoroutine == null)
        {
            _serverBroadcastCoroutine = StartCoroutine(ServerBroadcastLoop());
        }

        if (_isClient && _clientDiscoveryCoroutine == null)
        {
            _clientDiscoveryCoroutine = StartCoroutine(ClientDiscoveryLoop());
        }
    }
    #endregion
}
````using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using MidManStudio.OfflineLobby;
using Unity.Netcode;
using System.Linq;

namespace MidManStudio.UI
{
    public class OfflineLobbyUIManager : MonoBehaviour
    {
        [Header("Player Name Setup")]
        [SerializeField] private TMP_InputField playerNameInput;

        [Header("Main Menu UI (Play/Customize)")]
        [SerializeField] private Button playButton;
        [SerializeField] private Button customizeButton;
        [SerializeField] private Button exitButton;

        [Header("Play Area UI (Join/Create Navigation)")]
        [SerializeField] private Button joinButton;
        [SerializeField] private Button createButton;

        [Header("Join Area UI")]
        [SerializeField] private Transform lobbyListContainer;
        [SerializeField] private OfflineLobbyCard lobbyCardPrefab;
        [SerializeField] private Button refreshLobbiesButton;
        [SerializeField] private TextMeshProUGUI noLobbiesText;

        [Header("Create Area UI")]
        [SerializeField] private TMP_InputField lobbyNameInput;
        [SerializeField] private Button gameModeButton;
        [SerializeField] private Button mapButton;
        [SerializeField] private TextMeshProUGUI selectedGameModeText;
        [SerializeField] private TextMeshProUGUI selectedMapText;
        [SerializeField] private Toggle fillPlayerSpacesToggle;
        [SerializeField] private Button createLobbyConfirmButton;

        [Header("Lobby Room UI")]
        [SerializeField] private TextMeshProUGUI lobbyRoomNameText;
        [SerializeField] private TextMeshProUGUI lobbyRoomInfoText;
        [SerializeField] private Transform playerListContainer;
        [SerializeField] private OfflineLobbyPlayerCard playerCardPrefab;
        [SerializeField] private Button readyButton;
        [SerializeField] private Button startGameButton;
        [SerializeField] private Button leaveLobbyButton;
        [SerializeField] private TextMeshProUGUI statusText;

        [Header("Bot Info Display")]
        [SerializeField] private GameObject botInfoPanel; // Panel showing bot info
        [SerializeField] private TextMeshProUGUI botInfoText; // Text like "Remaining slots will be filled with bots in-game"

        [Header("Unified Popup System")]
        [SerializeField] private OfflineUnifiedLobbyHandler unifiedLobbyHandler;

        [Header("Game Settings")]
        [SerializeField] private AllLoadableScenes gameplayScene;

        [Header("Debug")]
        [SerializeField] private bool detailedLogging = true;

        // Current selections
        private OfflineGameMode selectedGameMode = OfflineGameMode.TeamDeathMatch;
        private OfflineGameMap selectedMap = OfflineGameMap.GrassyLand;

        // UI State
        private bool _isPlayerReady = false;
        private Dictionary<ulong, OfflineLobbyPlayerCard> _playerCards = new Dictionary<ulong, OfflineLobbyPlayerCard>();
        private Dictionary<string, OfflineLobbyCard> _lobbyCards = new Dictionary<string, OfflineLobbyCard>();

        #region Unity Lifecycle
        private void Awake()
        {
            string savedName = PlayerPrefs.GetString("PlayerName", "");
            if (!string.IsNullOrEmpty(savedName))
            {
                playerNameInput.text = savedName;
                OfflineLobbyManager.Instance.SetPlayerName(savedName);
            }

            SetupEventListeners();
            SetupUnifiedPopupSystem();

            LogMessage("Offline Lobby UI Manager initialized with unified popup system");
        }

        private void OnEnable()
        {
            SubscribeToEvents();
            LogMessage("Subscribed to Lobby Manager events");
        }

        private void OnDisable()
        {
            UnsubscribeFromEvents();
            LogMessage("Unsubscribed from Lobby Manager events");
        }
        #endregion

        #region Event Subscriptions
        private void SubscribeToEvents()
        {
            if (OfflineLobbyManager.Instance != null)
            {
                OfflineLobbyManager.Instance.OnLobbyDiscovered += HandleLobbyDiscovered;
                OfflineLobbyManager.Instance.OnLobbyRemoved += HandleLobbyRemoved;
                OfflineLobbyManager.Instance.OnPlayerJoined += HandlePlayerJoined;
                OfflineLobbyManager.Instance.OnPlayerLeft += HandlePlayerLeft;
                OfflineLobbyManager.Instance.OnPlayerReadyStatusChanged += HandlePlayerReadyStatusChanged;
                OfflineLobbyManager.Instance.OnJoinLobbyResult += HandleJoinLobbyResult;
                OfflineLobbyManager.Instance.OnHostLobbyResult += HandleHostLobbyResult;
                OfflineLobbyManager.Instance.OnLobbyDisbanded += HandleLobbyDisbanded;
            }

            if (NetworkScene.NetworkSceneManager.Instance != null)
            {
                NetworkScene.NetworkSceneManager.Instance.OnNetworkSceneLoadComplete += HandleSceneLoadComplete;
                NetworkScene.NetworkSceneManager.Instance.OnNetworkSceneLoadFailed += HandleSceneLoadFailed;
            }
        }

        private void UnsubscribeFromEvents()
        {
            if (OfflineLobbyManager.Instance != null)
            {
                OfflineLobbyManager.Instance.OnLobbyDiscovered -= HandleLobbyDiscovered;
                OfflineLobbyManager.Instance.OnLobbyRemoved -= HandleLobbyRemoved;
                OfflineLobbyManager.Instance.OnPlayerJoined -= HandlePlayerJoined;
                OfflineLobbyManager.Instance.OnPlayerLeft -= HandlePlayerLeft;
                OfflineLobbyManager.Instance.OnPlayerReadyStatusChanged -= HandlePlayerReadyStatusChanged;
                OfflineLobbyManager.Instance.OnJoinLobbyResult -= HandleJoinLobbyResult;
                OfflineLobbyManager.Instance.OnHostLobbyResult -= HandleHostLobbyResult;
                OfflineLobbyManager.Instance.OnLobbyDisbanded -= HandleLobbyDisbanded;
            }

            if (NetworkScene.NetworkSceneManager.Instance != null)
            {
                NetworkScene.NetworkSceneManager.Instance.OnNetworkSceneLoadComplete -= HandleSceneLoadComplete;
                NetworkScene.NetworkSceneManager.Instance.OnNetworkSceneLoadFailed -= HandleSceneLoadFailed;
            }
        }
        #endregion

        #region UI Event Handlers Setup
        private void SetupEventListeners()
        {
            if (playerNameInput != null)
            {
                playerNameInput.onEndEdit.AddListener(OnPlayerNameChanged);
            }

            if (playButton != null)
            {
                playButton.onClick.AddListener(() => {
                    OfflineLobbyStateManager.Instance.GoToPlayArea();
                    LogMessage("Play button clicked");
                });
            }

            if (customizeButton != null)
            {
                customizeButton.onClick.AddListener(() => {
                    LogMessage("Customize button clicked - Not implemented yet");
                });
            }

            if (exitButton != null)
            {
                exitButton.onClick.AddListener(() => {
                    LogMessage("Exit button clicked");
                    Application.Quit();
                });
            }

            if (joinButton != null)
            {
                joinButton.onClick.AddListener(() => {
                    OfflineLobbyStateManager.Instance.GoToJoinArea();
                    StartLobbyDiscovery();
                    LogMessage("Join button clicked");
                });
            }

            if (createButton != null)
            {
                createButton.onClick.AddListener(() => {
                    OfflineLobbyStateManager.Instance.GoToCreateArea();
                    LogMessage("Create button clicked");
                });
            }

            if (refreshLobbiesButton != null)
            {
                refreshLobbiesButton.onClick.AddListener(OnRefreshLobbiesButtonClicked);
            }

            if (gameModeButton != null)
            {
                gameModeButton.onClick.AddListener(() => {
                    ShowGameModeSelection();
                    LogMessage("Game mode button clicked - Opening unified popup for game modes");
                });
            }

            if (mapButton != null)
            {
                mapButton.onClick.AddListener(() => {
                    ShowMapSelection();
                    LogMessage("Map button clicked - Opening unified popup for maps");
                });
            }

            if (createLobbyConfirmButton != null)
            {
                createLobbyConfirmButton.onClick.AddListener(OnCreateLobbyConfirmClicked);
            }

            if (readyButton != null)
            {
                readyButton.onClick.AddListener(OnReadyButtonClicked);
            }

            if (startGameButton != null)
            {
                startGameButton.onClick.AddListener(OnStartGameButtonClicked);
            }

            if (leaveLobbyButton != null)
            {
                leaveLobbyButton.onClick.AddListener(OnLeaveLobbyButtonClicked);
            }

            if (fillPlayerSpacesToggle != null)
            {
                // CRITICAL FIX: Initialize toggle with current value BEFORE adding listener
                fillPlayerSpacesToggle.SetIsOnWithoutNotify(OfflineLobbyManager.Instance.GetFillPlayerSpaces());

                // Now add the listener
                fillPlayerSpacesToggle.onValueChanged.AddListener(OnFillPlayerSpacesToggled);

                LogMessage($"Fill player spaces toggle initialized: {fillPlayerSpacesToggle.isOn}");
            }
        }

        private void SetupUnifiedPopupSystem()
        {
            if (unifiedLobbyHandler != null)
            {
                unifiedLobbyHandler.OnGameModeSelected += OnGameModeSelected;
                unifiedLobbyHandler.OnMapSelected += OnMapSelected;
                LogMessage("Unified popup system connected successfully");
            }
            else
            {
                LogError("OfflineUnifiedLobbyHandler is not assigned in the inspector!");
            }
            
            UpdateSelectedGameModeDisplay();
            UpdateSelectedMapDisplay();
        }

        private void ShowGameModeSelection()
        {
            if (unifiedLobbyHandler != null)
            {
                unifiedLobbyHandler.ShowGameModeSelection();
                LogMessage("Showing game mode selection via unified popup");
            }
            else
            {
                LogError("Cannot show game mode selection: OfflineUnifiedLobbyHandler is not assigned");
            }
        }

        private void ShowMapSelection()
        {
            if (unifiedLobbyHandler != null)
            {
                unifiedLobbyHandler.ShowMapSelection();
                LogMessage("Showing map selection via unified popup");
            }
            else
            {
                LogError("Cannot show map selection: OfflineUnifiedLobbyHandler is not assigned");
            }
        }
        #endregion

        #region UI Event Handlers
        private void OnPlayerNameChanged(string newName)
        {
            if (!string.IsNullOrEmpty(newName))
            {
                PlayerPrefs.SetString("PlayerName", newName);
                OfflineLobbyManager.Instance.SetPlayerName(newName);
                LogMessage($"Player name changed to: {newName}");
            }
        }

        private void OnRefreshLobbiesButtonClicked()
        {
            LogMessage("Refresh Lobbies button clicked");
            ClearLobbyList();
            OfflineLobbyManager.Instance.StopSearchingForLobbies();
            OfflineLobbyManager.Instance.StartSearchingForLobbies();

            if (noLobbiesText) noLobbiesText.text = "Searching for nearby lobbies...";
        }

        private void OnGameModeSelected(OfflineGameMode gameMode)
        {
            selectedGameMode = gameMode;
            UpdateSelectedGameModeDisplay();
            LogMessage($"Game mode selected via unified popup: {gameMode}");
        }

        private void OnMapSelected(OfflineGameMap map)
        {
            selectedMap = map;
            UpdateSelectedMapDisplay();
            LogMessage($"Map selected via unified popup: {map}");
        }

        private void OnCreateLobbyConfirmClicked()
        {
            string lobbyName = lobbyNameInput.text;
            if (string.IsNullOrEmpty(lobbyName))
            {
                lobbyName = "Local Game";
                LogMessage("Using default lobby name: Local Game");
            }

            // CRITICAL FIX: Set fill player spaces BEFORE starting the lobby
            if (fillPlayerSpacesToggle != null && OfflineLobbyManager.Instance != null)
            {
                bool fillEnabled = fillPlayerSpacesToggle.isOn;
                OfflineLobbyManager.Instance.SetFillPlayerSpaces(fillEnabled);
                LogMessage($"Set fill player spaces to: {fillEnabled} BEFORE hosting lobby");
            }

            int maxPlayers = OfflineLobbyManager.Instance.GetMaxPlayersForGameMode(selectedGameMode);
            LogMessage($"Creating lobby: {lobbyName} with {maxPlayers} max players, mode: {selectedGameMode}, map: {selectedMap}");

            OfflineLobbyManager.Instance.StartHostingLobby(lobbyName, maxPlayers, selectedGameMode, selectedMap);
        }

        private void OnFillPlayerSpacesToggled(bool enabled)
        {
            LogMessage($"Fill player spaces toggled to: {enabled}");

            // CRITICAL: Update the OfflineLobbyManager setting
            if (OfflineLobbyManager.Instance != null)
            {
                OfflineLobbyManager.Instance.SetFillPlayerSpaces(enabled);
                LogMessage($"OfflineLobbyManager.SetFillPlayerSpaces called with: {enabled}");
            }
            else
            {
                LogError("Cannot update fill player spaces: OfflineLobbyManager.Instance is null");
            }

            // Update bot info display in lobby room (if currently in lobby)
            if (OfflineLobbyManager.Instance != null && OfflineLobbyManager.Instance.IsHost())
            {
                UpdateBotInfoDisplay();
                LogMessage("Updated bot info display after toggle change");
            }
        }

        private void OnReadyButtonClicked()
        {
            _isPlayerReady = !_isPlayerReady;
            readyButton.GetComponentInChildren<TextMeshProUGUI>().text = _isPlayerReady ? "Unready" : "Ready";

            ulong localClientId = NetworkManager.Singleton.LocalClientId;
            LogMessage($"Player ready status changed to: {_isPlayerReady}");
            OfflineLobbyManager.Instance.SetPlayerReady(localClientId, _isPlayerReady);
        }

        private void OnStartGameButtonClicked()
        {
            LogMessage("Start Game button clicked");
            if (OfflineLobbyManager.Instance.AreAllPlayersReady())
            {
                statusText.text = "Starting game...";
                LogMessage("All players ready. Starting game with scene: " + gameplayScene.ToString());
                OfflineLobbyManager.Instance.StartGame(gameplayScene);
            }
            else
            {
                statusText.text = "Not all players are ready!";
                LogWarning("Cannot start game: Not all players are ready");
            }
        }

        private void OnLeaveLobbyButtonClicked()
        {
            LogMessage("Leave Lobby button clicked");
            if (OfflineLobbyManager.Instance.IsHost())
            {
                LogMessage("Host is disbanding the lobby");
                OfflineLobbyManager.Instance.StopHostingLobby();
            }
            else
            {
                LogMessage("Client is leaving the lobby");
                OfflineLobbyManager.Instance.LeaveLobby();
            }
            OfflineLobbyStateManager.Instance.GoToMainMenu();
        }
        #endregion

        #region Lobby Manager Event Handlers
        private void HandleLobbyDiscovered(OfflineLobbyData lobbyData)
        {
            LogMessage($"Lobby discovered: {lobbyData.LobbyName} at {lobbyData.HostAddress}:{lobbyData.Port}");

            if (noLobbiesText && lobbyListContainer.childCount == 0)
            {
                noLobbiesText.text = "";
            }

            string lobbyKey = $"{lobbyData.HostAddress}:{lobbyData.Port}";

            if (!_lobbyCards.ContainsKey(lobbyKey))
            {
                GameObject lobbyCardObj = Instantiate(lobbyCardPrefab.gameObject, lobbyListContainer);
                OfflineLobbyCard lobbyCard = lobbyCardObj.GetComponent<OfflineLobbyCard>();
                _lobbyCards.Add(lobbyKey, lobbyCard);

                lobbyCard.Initialize(lobbyData, lobbyKey);
                lobbyCard.OnJoinLobbyRequested += OnJoinLobbyRequested;

                LogMessage($"Created new lobby card for {lobbyData.LobbyName}");
            }
            else
            {
                _lobbyCards[lobbyKey].UpdateLobbyData(lobbyData);
                LogMessage($"Updated existing lobby card for {lobbyData.LobbyName}");
            }
        }

        private void OnJoinLobbyRequested(OfflineLobbyData lobbyData)
        {
            LogMessage($"Join requested for lobby: {lobbyData.LobbyName}");
            statusText.text = $"Joining {lobbyData.LobbyName}...";
            OfflineLobbyManager.Instance.StopSearchingForLobbies();
            OfflineLobbyManager.Instance.JoinLobby(lobbyData);
        }

        private void HandleLobbyRemoved(string lobbyKey)
        {
            LogMessage($"Lobby removed with key: {lobbyKey}");

            if (_lobbyCards.ContainsKey(lobbyKey))
            {
                if (_lobbyCards[lobbyKey] != null)
                {
                    _lobbyCards[lobbyKey].OnJoinLobbyRequested -= OnJoinLobbyRequested;
                    Destroy(_lobbyCards[lobbyKey].gameObject);
                    LogMessage($"Destroyed lobby card for key: {lobbyKey}");
                }

                _lobbyCards.Remove(lobbyKey);

                if (noLobbiesText && lobbyListContainer.childCount == 0)
                {
                    noLobbiesText.text = "No lobbies found nearby";
                }
            }
        }

        private void HandlePlayerJoined(PlayerInfo playerInfo)
        {
            // CRITICAL: Only create player cards for REAL players (not bots)
            if (playerInfo.IsBot)
            {
                LogMessage($"Bot info received but NOT creating player card: {playerInfo.PlayerName}");
                return;
            }

            LogMessage($"Player joined: {playerInfo.PlayerName} (ID: {playerInfo.ClientId})");

            if (playerListContainer != null && playerCardPrefab != null)
            {
                if (!_playerCards.ContainsKey(playerInfo.ClientId))
                {
                    GameObject playerCardObj = Instantiate(playerCardPrefab.gameObject, playerListContainer);
                    OfflineLobbyPlayerCard playerCard = playerCardObj.GetComponent<OfflineLobbyPlayerCard>();
                    _playerCards.Add(playerInfo.ClientId, playerCard);

                    playerCard.Initialize(playerInfo);
                    playerCard.OnPlayerCardSelected += OnPlayerCardSelected;

                    LogMessage($"Created player card for {playerInfo.PlayerName}");
                }

                UpdateLobbyRoomInfo();
            }
        }

        private void OnPlayerCardSelected(ulong clientId)
        {
            LogMessage($"Player card selected for client ID: {clientId}");
        }

        private void HandlePlayerLeft(ulong clientId)
        {
            LogMessage($"Player left with client ID: {clientId}");

            if (_playerCards.ContainsKey(clientId))
            {
                if (_playerCards[clientId] != null)
                {
                    _playerCards[clientId].OnPlayerCardSelected -= OnPlayerCardSelected;
                    Destroy(_playerCards[clientId].gameObject);
                    LogMessage($"Destroyed player card for client ID: {clientId}");
                }

                _playerCards.Remove(clientId);
                UpdateLobbyRoomInfo();
            }
        }

        private void HandlePlayerReadyStatusChanged(PlayerInfo playerInfo)
        {
            // Only update cards for real players
            if (playerInfo.IsBot)
            {
                LogMessage($"Bot ready status changed (ignored in UI): {playerInfo.PlayerName}");
                return;
            }

            LogMessage($"Player ready status changed: {playerInfo.PlayerName} is now {(playerInfo.IsReady ? "ready" : "not ready")}");

            if (_playerCards.ContainsKey(playerInfo.ClientId))
            {
                _playerCards[playerInfo.ClientId].UpdateReadyStatus(playerInfo.IsReady);
                UpdateStartButtonState();
            }
        }

        private void HandleJoinLobbyResult(bool success)
        {
            if (success)
            {
                LogMessage("Successfully joined lobby");
                OfflineLobbyStateManager.Instance.GoToLobbyRoom();
                statusText.text = "Joined lobby successfully";

                _isPlayerReady = false;
                readyButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";

                UpdateLobbyRoomInfo();
                UpdateBotInfoDisplay(); // Show bot info if enabled
                startGameButton.gameObject.SetActive(false);

                // Hide fill toggle for clients
                if (fillPlayerSpacesToggle != null)
                {
                    fillPlayerSpacesToggle.gameObject.SetActive(false);
                }
            }
            else
            {
                LogError("Failed to join lobby");
                statusText.text = "Failed to join lobby";
                OfflineLobbyStateManager.Instance.GoToPlayArea();
            }
        }

        private void HandleHostLobbyResult(bool success)
        {
            if (success)
            {
                LogMessage("Successfully created and joined lobby as host");
                OfflineLobbyStateManager.Instance.GoToLobbyRoom();
                statusText.text = "Lobby created successfully";

                _isPlayerReady = false;
                readyButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";

                UpdateLobbyRoomInfo();
                UpdateBotInfoDisplay();
                startGameButton.gameObject.SetActive(true);

                // CRITICAL FIX: Show toggle for host and initialize it
                if (fillPlayerSpacesToggle != null)
                {
                    fillPlayerSpacesToggle.gameObject.SetActive(true);

                    // Get current value from LobbyManager and set toggle without triggering callback
                    bool currentValue = OfflineLobbyManager.Instance.GetFillPlayerSpaces();
                    fillPlayerSpacesToggle.SetIsOnWithoutNotify(currentValue);

                    LogMessage($"Host: Fill player spaces toggle set to: {currentValue}");
                }
            }
            else
            {
                LogError("Failed to create lobby");
                statusText.text = "Failed to create lobby";
                OfflineLobbyStateManager.Instance.GoToPlayArea();
            }
        }

        private void HandleLobbyDisbanded()
        {
            LogMessage("Lobby has been disbanded");
            statusText.text = "The lobby has been disbanded";
            OfflineLobbyStateManager.Instance.GoToMainMenu();

            foreach (var playerCard in _playerCards.Values)
            {
                if (playerCard != null)
                {
                    playerCard.OnPlayerCardSelected -= OnPlayerCardSelected;
                    Destroy(playerCard.gameObject);
                }
            }

            _playerCards.Clear();
            LogMessage("Cleared all player cards");
        }

        private void HandleSceneLoadComplete(AllLoadableScenes scene)
        {
            LogMessage($"Scene load complete: {scene}");
            statusText.text = "Game started!";
        }

        private void HandleSceneLoadFailed(string errorMessage)
        {
            LogError($"Scene load failed: {errorMessage}");
            statusText.text = $"Failed to start game: {errorMessage}";
        }
        #endregion

        #region UI Utility Methods
        private void StartLobbyDiscovery()
        {
            ClearLobbyList();
            OfflineLobbyManager.Instance.StartSearchingForLobbies();
            if (noLobbiesText) noLobbiesText.text = "Searching for nearby lobbies...";
        }

        private void ClearLobbyList()
        {
            LogMessage("Clearing lobby list");
            foreach (var lobbyCard in _lobbyCards.Values)
            {
                if (lobbyCard != null)
                {
                    lobbyCard.OnJoinLobbyRequested -= OnJoinLobbyRequested;
                    Destroy(lobbyCard.gameObject);
                }
            }

            _lobbyCards.Clear();
            LogMessage($"Cleared lobby cards");
        }

        private void UpdateLobbyRoomInfo()
        {
            var players = OfflineLobbyManager.Instance.GetCurrentPlayers();
            int realPlayerCount = players.Count(p => !p.IsBot); // Count only real players

            if (lobbyRoomNameText) lobbyRoomNameText.text = "Lobby Room";

            // Show only real player count in lobby
            if (lobbyRoomInfoText)
            {
                lobbyRoomInfoText.text = $"Players: {realPlayerCount}";
            }

            LogMessage($"Updated lobby info: {realPlayerCount} real players");
            UpdateStartButtonState();
        }

        // NEW: Update bot info display
        private void UpdateBotInfoDisplay()
        {
            if (botInfoPanel == null || botInfoText == null) return;

            bool fillEnabled = OfflineLobbyManager.Instance.GetFillPlayerSpaces();

            if (fillEnabled)
            {
                int botsNeeded = OfflineLobbyManager.Instance.CalculateBotsNeeded();

                if (botsNeeded > 0)
                {
                    botInfoPanel.SetActive(true);
                    botInfoText.text = $" {botsNeeded} bot{(botsNeeded > 1 ? "s" : "")} will join in the game";
                    LogMessage($"Displaying bot info: {botsNeeded} bots will be added in gameplay");
                }
                else
                {
                    botInfoPanel.SetActive(false);
                    LogMessage("No bots needed - lobby is full with real players");
                }
            }
            else
            {
                botInfoPanel.SetActive(false);
                LogMessage("Fill with bots disabled - hiding bot info panel");
            }
        }

        private void UpdateStartButtonState()
        {
            if (!startGameButton) return;

            bool canStart = OfflineLobbyManager.Instance.IsHost() && OfflineLobbyManager.Instance.AreAllPlayersReady();
            startGameButton.interactable = canStart;

            if (startGameButton.interactable)
            {
                startGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start Game";
                LogMessage("Start game button enabled");
            }
            else if (OfflineLobbyManager.Instance.IsHost())
            {
                startGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Waiting for players...";
                LogMessage("Start game button disabled: Waiting for players");
            }
        }

        private void UpdateSelectedGameModeDisplay()
        {
            if (selectedGameModeText != null)
            {
                selectedGameModeText.text = OfflineLobbyManager.Instance.GetGameModeName(selectedGameMode);
            }
        }

        private void UpdateSelectedMapDisplay()
        {
            if (selectedMapText != null)
            {
                selectedMapText.text = OfflineLobbyManager.Instance.GetGameMapName(selectedMap);
            }
        }

        [ContextMenu("Verify Fill Bots Setting")]
        private void VerifyFillBotsSetting()
        {
            if (fillPlayerSpacesToggle != null && OfflineLobbyManager.Instance != null)
            {
                bool toggleValue = fillPlayerSpacesToggle.isOn;
                bool managerValue = OfflineLobbyManager.Instance.GetFillPlayerSpaces();

                Debug.Log($"[VERIFICATION] Toggle value: {toggleValue}");
                Debug.Log($"[VERIFICATION] Manager value: {managerValue}");
                Debug.Log($"[VERIFICATION] Values match: {toggleValue == managerValue}");

                if (toggleValue != managerValue)
                {
                    Debug.LogWarning("[VERIFICATION] VALUES DO NOT MATCH! Syncing...");
                    OfflineLobbyManager.Instance.SetFillPlayerSpaces(toggleValue);
                }
            }
        }
        #endregion

        #region Logging Methods
        private void LogMessage(string message)
        {
            if (detailedLogging)
            {
                Debug.Log($"[<color=blue>OfflineLobbyUI</color>] {message}");
            }
        }

        private void LogWarning(string message)
        {
            Debug.LogWarning($"[<color=yellow>OfflineLobbyUI</color>] {message}");
        }

        private void LogError(string message)
        {
            Debug.LogError($"[<color=red>OfflineLobbyUI</color>] {message}");
        }
        #endregion

        #region Cleanup
        private void OnDestroy()
        {
            if (unifiedLobbyHandler != null)
            {
                unifiedLobbyHandler.OnGameModeSelected -= OnGameModeSelected;
                unifiedLobbyHandler.OnMapSelected -= OnMapSelected;
            }
        }
        #endregion
    }
}`