// // /*=============================================================================== // // Copyright (C) 2024 PhantomsXR Ltd. All Rights Reserved. // // // // This file is part of the Phantom.XRMOD.PhotonModule.Runtime. // // // // The AVPPlatform cannot be copied, distributed, or made available to // // third-parties for commercial purposes without written permission of PhantomsXR Ltd. // // // // Contact info@phantomsxr.com for licensing requests. // // ===============================================================================*/ #if FUSION2 using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Fusion; using Fusion.Photon.Realtime; using Fusion.Sockets; using Phantom.XRMOD.Core.Runtime; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace Phantom.XRMOD.PhotonModule.Runtime { public class PhotonServerAPI : IDisposable { public Action OnGameStartCallback; private NetworkProjectConfig networkProjectConfig; private NetworkEvents networkEvents; private NetworkProjectConfigAsset networkProjectConfigAsset; private NetworkRunner clientRunner; private INetworkSceneManager sceneManager; private INetworkObjectProvider objectProvider; private CancellationTokenSource cancellationTokenSource; private CancellationToken cancellationTokenForJoinLobby; private CancellationToken cancellationToken; private FusionAppSettings fusionAppSettings; private static readonly Dictionary _NETWORK_PREFAB_IDS = new(); private bool connectingSafeCheck; private static PhotonServerAPI _INSTANCE; public static PhotonServerAPI GetInstance => _INSTANCE ??= new PhotonServerAPI(); /// /// The index number used for the last created peer. /// public int LastCreatedClientIndex { get; internal set; } public NetworkRunner GetNetworkRunner => clientRunner; public NetworkEvents GetNetworkEvents => networkEvents; /// /// Will automatically enable once peers have finished connecting. /// [InlineHelp] public bool AlwaysShowStats; private NetworkObjectBaker _baker; public NetworkObjectBaker Baker => _baker ??= new NetworkObjectBaker(); private PhotonServerAPI() { foreach ((NetworkPrefabId, INetworkPrefabSource) tmp_Entry in NetworkProjectConfig.Global.PrefabTable .GetEntries()) { _NETWORK_PREFAB_IDS.TryAdd(tmp_Entry.Item1, tmp_Entry.Item2); } } public NetworkRunner Initialize() { if (networkProjectConfig == null) { networkProjectConfigAsset = Resources.Load(nameof(NetworkProjectConfig)); networkProjectConfig = networkProjectConfigAsset.Config; } if (clientRunner) return clientRunner; clientRunner = Object.FindFirstObjectByType(); if (clientRunner == null) { var tmp_FusionRunnerPrefab = Resources.Load("Prefabs/FusionRunnerPrefab"); clientRunner = Object.Instantiate(tmp_FusionRunnerPrefab).GetComponent(); } clientRunner.name = $"Client {(Char) (65 + LastCreatedClientIndex++)}"; networkEvents = clientRunner.GetBehaviour(); sceneManager = clientRunner.GetComponent(); objectProvider = clientRunner.GetComponent(); return clientRunner; } private void Deinitialize() { if (clientRunner) clientRunner.SafeDestroy(); sceneManager = null; objectProvider = null; networkProjectConfigAsset = null; networkProjectConfig = null; _INSTANCE = null; } /// /// Returns the player is connect to server. /// public bool IsConnected => clientRunner && clientRunner.IsRunning; /// /// Returns the player round trip time (ping) in millisecond. /// public int Ping => (int) (IsConnected ? clientRunner.GetPlayerRtt(clientRunner.LocalPlayer) * 1000 : 0); /// /// Join the lobby. /// If you want to get all game session you must joined lobby first. /// You could register the event to check the game session list. /// /// Lobby id is necessary. Lobby Id must same as your game session lobby id. /// Lobby type must same as your game session type /// The game result of join the lobby public async Task JoinLobby(string _lobbyId, SessionLobby _lobbyType) { if (clientRunner == null) { Initialize(); } if (cancellationTokenSource != null) cancellationTokenSource.Cancel(); cancellationTokenSource?.Dispose(); cancellationTokenSource = new CancellationTokenSource(); cancellationToken = cancellationTokenSource.Token; StartGameResult tmp_StartGameResult = null; if (fusionAppSettings != null) { tmp_StartGameResult = await clientRunner.JoinSessionLobby(_lobbyType, _lobbyId, customAppSettings: fusionAppSettings, cancellationToken: cancellationToken); } else { tmp_StartGameResult = await clientRunner.JoinSessionLobby(_lobbyType, _lobbyId, cancellationToken: cancellationToken); } return tmp_StartGameResult; } /// /// Create or join a session. /// /// Your game name /// The session code /// /// The max player in this session /// Anyone can join /// /// /// The ConnectResult of create or join game session public async Task CreateOrJoinSession(string _gameName, string _sessionCode = null, GameMode _gameMode = GameMode.Shared, bool _enableClientSessionCreation = false, Dictionary _customProperties = null, int _maxPlayer = 2, bool _isPublic = true) { if (clientRunner == null) { Initialize(); } if (connectingSafeCheck) return new ConnectResult() {CustomResultHandling = true, Success = false, FailReason = ConnectFailReason.None}; connectingSafeCheck = true; cancellationTokenSource?.Dispose(); cancellationTokenSource = new CancellationTokenSource(); cancellationToken = cancellationTokenSource.Token; _customProperties ??= new Dictionary(); _customProperties?.Add("GameType", _gameName); var tmp_StartGameArgs = new StartGameArgs { GameMode = _gameMode, Address = NetAddress.Any(), PlayerCount = _maxPlayer, IsVisible = _isPublic, SessionProperties = _customProperties, OnGameStarted = OnGameStartCallback, StartGameCancellationToken = cancellationToken, EnableClientSessionCreation = _enableClientSessionCreation }; tmp_StartGameArgs.SessionName = _sessionCode; if (fusionAppSettings != null) { tmp_StartGameArgs.CustomPhotonAppSettings = fusionAppSettings; } var tmp_Scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex); var tmp_StartGameResult = await InitializeNetworkRunner(tmp_StartGameArgs, tmp_Scene); connectingSafeCheck = false; return new ConnectResult() { Success = tmp_StartGameResult.Ok, FailReason = ResolveConnectFailReason(tmp_StartGameResult.ShutdownReason) }; } //// ---> Report a new feature: Add game mode for parameters /// ----> Report a new feature: Add EnableClientSessionCreation parameters for this fun /// /// Join the random game session /// /// Join state. More information . public async Task JoinRandomSession(string _gameName, GameMode _gameMode = GameMode.Shared, bool _enableClientSessionCreation = false, Dictionary _sessionInfos = null) { if (clientRunner == null) { Initialize(); } if (connectingSafeCheck) return new ConnectResult() {CustomResultHandling = true, Success = false, FailReason = ConnectFailReason.None}; connectingSafeCheck = true; _sessionInfos ??= new(); _sessionInfos.Add("GameType", _gameName); var tmp_StartGameArgs = new StartGameArgs { GameMode = _gameMode, MatchmakingMode = MatchmakingMode.RandomMatching, SessionProperties = _sessionInfos, EnableClientSessionCreation = _enableClientSessionCreation, }; if (fusionAppSettings != null) { tmp_StartGameArgs.CustomPhotonAppSettings = fusionAppSettings; } var tmp_Scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex); var tmp_StartGameResult = await InitializeNetworkRunner(tmp_StartGameArgs, tmp_Scene); connectingSafeCheck = false; return new ConnectResult() { Success = tmp_StartGameResult.ShutdownReason == ShutdownReason.Ok, FailReason = ResolveConnectFailReason(tmp_StartGameResult.ShutdownReason) }; } /// /// Disconnect to game server /// /// The disconnect reason. More information see . public async void DisconnectAsync(int _reason = 1) { if (cancellationTokenSource != null) cancellationTokenSource.Cancel(); if (clientRunner) await clientRunner.Shutdown(shutdownReason: ResolveShutdownReason(_reason)); connectingSafeCheck = false; for (int tmp_Idx = SceneManager.sceneCount - 1; tmp_Idx > 0; tmp_Idx--) { _ = SceneManager.UnloadSceneAsync(SceneManager.GetSceneAt(tmp_Idx)); } if (clientRunner) clientRunner.gameObject.SafeDestroy(); } /// /// This will register any prefab, but it needs to be loaded first (from whatever the source) /// /// The Unity game object /// The network id of game object prefab /// The register exception public NetworkPrefabId RegisterNetworkPrefab(GameObject _prefab) { if (!_prefab.TryGetComponent(out NetworkObject tmp_NetworkObject)) return default; var tmp_Source = new NetworkPrefabSourceStaticLazy() { Object = tmp_NetworkObject, }; if (NetworkProjectConfig.Global.PrefabTable.TryAddSource(tmp_Source, out var tmp_NetworkPrefabId)) { Debug.Log($"{tmp_NetworkPrefabId} register into network prefab table."); return tmp_NetworkPrefabId; } Debug.LogError($"Register Network prefab({_prefab.name}) was failed."); return default; } /// /// Select a region /// /// Server region /// public void BuildNetworkConfig(RegionEnum _region, NetworkSettingConfig _config = default) { _config ??= new NetworkSettingConfig(); fusionAppSettings = BuildCustomAppSetting(_region, _config); } private int ResolveConnectFailReason(ShutdownReason _reason) { switch (_reason) { case ShutdownReason.Ok: case ShutdownReason.OperationCanceled: return ConnectFailReason.UserRequest; case ShutdownReason.DisconnectedByPluginLogic: case ShutdownReason.Error: return ConnectFailReason.Disconnect; default: return ConnectFailReason.None; } } private ShutdownReason ResolveShutdownReason(int _reason) { switch (_reason) { case ConnectFailReason.UserRequest: return ShutdownReason.Ok; case ConnectFailReason.ApplicationQuit: return ShutdownReason.Ok; case ConnectFailReason.Disconnect: return ShutdownReason.DisconnectedByPluginLogic; default: return ShutdownReason.Error; } } private Task InitializeNetworkRunner(StartGameArgs _startGameArgs, SceneRef _scene) { if (sceneManager == null) { Debug.Log( $"NetworkRunner does not have any component implementing {nameof(INetworkSceneManager)} interface, adding {nameof(NetworkSceneManagerDefault)}.", clientRunner); sceneManager = clientRunner.gameObject.AddComponent(); } if (objectProvider == null) { Debug.Log( $"NetworkRunner does not have any component implementing {nameof(INetworkObjectProvider)} interface, adding {nameof(NetworkObjectProviderDefault)}.", clientRunner); objectProvider = clientRunner.gameObject.AddComponent(); } var tmp_SceneInfo = new NetworkSceneInfo(); if (_scene.IsValid) { tmp_SceneInfo.AddSceneRef(_scene, LoadSceneMode.Additive); } _startGameArgs.ObjectProvider = objectProvider; _startGameArgs.Scene = tmp_SceneInfo; _startGameArgs.SceneManager = sceneManager; _startGameArgs.Address = NetAddress.Any(); var tmp_Task = clientRunner.StartGame(_startGameArgs); return tmp_Task; } private FusionAppSettings BuildCustomAppSetting(RegionEnum _region, NetworkSettingConfig _config) { var tmp_AppSettings = PhotonAppSettings.Global.AppSettings.GetCopy(); tmp_AppSettings.UseNameServer = _config.UseNameServer; if (!string.IsNullOrEmpty(_config.AppVersion)) tmp_AppSettings.AppVersion = _config.AppVersion; if (!string.IsNullOrEmpty(_config.CustomAppIDFusion)) { tmp_AppSettings.AppIdFusion = _config.CustomAppIDFusion; } if (!string.IsNullOrEmpty(_config.CustomAppIdVoice)) { tmp_AppSettings.AppIdVoice = _config.CustomAppIdVoice; } if (!string.IsNullOrEmpty(_config.CustomAppIdChat)) { tmp_AppSettings.AppIdChat = _config.CustomAppIdChat; } switch (_region) { case RegionEnum.CN: tmp_AppSettings.FixedRegion = "cn"; tmp_AppSettings.Server = "ns.photonengine.cn"; break; case RegionEnum.BEST: tmp_AppSettings.FixedRegion = string.Empty; tmp_AppSettings.Server = string.Empty; break; case RegionEnum.CUSTOM: tmp_AppSettings.FixedRegion = string.Empty; break; case RegionEnum.DEDICATED: tmp_AppSettings.Server = _config.CustomServer; tmp_AppSettings.Port = _config.Port; break; default: tmp_AppSettings.FixedRegion = _region.ToString().ToLower(); break; } return tmp_AppSettings; } public void Dispose() { NetworkProjectConfig.Global.PrefabTable.Clear(); foreach (KeyValuePair tmp_Source in _NETWORK_PREFAB_IDS) { if (NetworkProjectConfig.Global.PrefabTable.TryAddSource(tmp_Source.Value, out var tmp_NetworkPrefabId)) { Debug.Log($"{tmp_NetworkPrefabId} register into network prefab table."); } } _NETWORK_PREFAB_IDS.Clear(); Deinitialize(); } } public enum RegionEnum { BEST, ASIA, AU, CAE, CN, EU, HK, IN, JP, SA, KR, TR, UAE, US, USW, USSC, CUSTOM, DEDICATED } public class NetworkSettingConfig { public string CustomAppIDFusion = string.Empty; public string CustomAppIdVoice = string.Empty; public string CustomAppIdChat = string.Empty; public string AppVersion = "0.1.0"; public string CustomServer = string.Empty; public int Port = 0; public bool UseNameServer = true; } } #endif