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
}
}`