using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; using Epic.OnlineServices; using Epic.OnlineServices.Auth; using Epic.OnlineServices.Connect; using Epic.OnlineServices.Lobby; using Epic.OnlineServices.Logging; using Epic.OnlineServices.Platform; using Epic.OnlineServices.RTC; using Epic.OnlineServices.RTCAudio; using UnityEngine; using Credentials = Epic.OnlineServices.Auth.Credentials; using LoginCallbackInfo = Epic.OnlineServices.Auth.LoginCallbackInfo; using LoginOptions = Epic.OnlineServices.Auth.LoginOptions; #if UNITY_GAMECORE using Unity.GameCore; using UnityEngine.GameCore; #elif UNITY_PS4 || UNITY_PS5 using Unity.PSN.PS5; using Unity.PSN.PS5.Auth; using Unity.PSN.PS5.Aysnc; #if UNITY_PS4 using PSInput = UnityEngine.PS4.PS4Input; #elif UNITY_PS5 using PSInput = UnityEngine.PS5.PS5Input; #endif #endif public class EpicVoiceChatTest : MonoBehaviour { // We intend to use predetermined lobby names generated from game-specific match/team/squad information, // so to simulate this we use a static GUID here, as opposed to letting Epic's backend automatically generate a random lobby ID. // This also allows multiple clients to connect to the same lobby without having to communicate the ID among each other. private const string DebugLobbyId = "a0f6a51f-6b61-4c95-9ed8-a1d508fe4eb6"; [SerializeField] private string devAuthAddress; private readonly StringBuilder status = new StringBuilder("Status:\n"); private PlatformInterface platformInterface; private AuthInterface authInterface; private ConnectInterface connectInterface; private LobbyInterface lobbyInterface; private RTCInterface rtcInterface; private RTCAudioInterface audioInterface; private EpicAccountId localEpicAccountId; private ProductUserId localProductUserId; private EOSVoiceChat voiceChat; private string xstsToken; private string psnIdToken; private string XAudio29DllPath => #if UNITY_EDITOR Path.Combine(Application.dataPath, @"Plugins\EpicOnlineServices\Bin\x64\xaudio2_9redist.dll"); #else Path.Combine(Application.dataPath, @"Plugins\x86_64\xaudio2_9redist.dll"); #endif IEnumerator Start() { #if UNITY_EDITOR && UNITY_STANDALONE LoadLibrary(); #elif UNITY_GAMECORE int hresult = SDK.XGameRuntimeInitialize(); if (HR.FAILED(hresult)) { Debug.LogError($"Error initializing game runtime, hresult = 0x{hresult:X}"); yield break; } Debug.Log("GXDK game runtime initialized."); hresult = SDK.XBL.XblInitialize(GameCoreSettings.SCID); if (HR.FAILED(hresult)) { Debug.LogError($"Error initializing Xbox Live services, hresult = 0x{hresult:X}"); yield break; } hresult = SDK.XGameGetXboxTitleId(out uint titleId); if (HR.FAILED(hresult)) { Debug.LogError($"Error obtaining Xbox Title ID, hresult = 0x{hresult:X}"); yield break; } Debug.Log("Xbox Live services initialized."); // This delay is just here to give us some time to connect the debugger yield return new WaitForSeconds(5f); #elif UNITY_PS4 || UNITY_PS5 try { var initResult = Main.Initialize(); if (initResult.Initialized) { Debug.Log($"PSN Initialized\nPlugin SDK Version: {initResult.SceSDKVersion}"); } else { Debug.LogWarning("PSN not initialized!"); yield break; } } catch (PSNException ex) { Debug.LogError("Exception during PSN initialization: " + ex.ExtendedMessage); Debug.LogException(ex); yield break; } // This delay is just here to give us some time to connect the debugger yield return new WaitForSeconds(5f); #endif #if !UNITY_STANDALONE EOSNativeHelper.GetMemoryFunctions(out var allocFunc, out var reallocFunc, out var releaseFunc); #endif var initOptions = new InitializeOptions { ProductName = "WW1Test", ProductVersion = "1.0.0.0", #if !UNITY_STANDALONE // EOS SDK on consoles will not initialize without these memory management function pointers AllocateMemoryFunction = allocFunc, ReallocateMemoryFunction = reallocFunc, ReleaseMemoryFunction = releaseFunc, #endif }; var result = PlatformInterface.Initialize(ref initOptions); if (result != Result.Success) { Debug.LogError("Failed to initialize EOS, result = " + result); status.AppendLine("EOS initialization failed..."); yield break; } LoggingInterface.SetLogLevel(LogCategory.AllCategories, LogLevel.Warning); LoggingInterface.SetCallback(OnEOSLogMessage); #if UNITY_STANDALONE_WIN Debug.Log("XAudio library path: " + XAudio29DllPath); var options = new WindowsOptions // Okay so this will need to be platform-specific { ProductId = "b21a28c2c5404c8099d72f5a28c59c16", SandboxId = "b630f2c3933a4838a971ce53d5b0db3b", DeploymentId = "9a36f589572c492fbee14bd299173c12", ClientCredentials = new ClientCredentials { ClientId = "xyza7891UOFoUhfvfbKgO2xRCIiuAIjH", ClientSecret = "NArwIQT1laFfsS7fdcN1MKDdgwy490w9MBJsAlHN4QI", }, Flags = PlatformFlags.DisableOverlay, CacheDirectory = null, EncryptionKey = null, IsServer = false, RTCOptions = new WindowsRTCOptions // This is also platform-specific { PlatformSpecificOptions = new WindowsRTCOptionsPlatformSpecificOptions { XAudio29DllPath = XAudio29DllPath } }, }; #else var options = new Options { ProductId = "b21a28c2c5404c8099d72f5a28c59c16", SandboxId = "b630f2c3933a4838a971ce53d5b0db3b", DeploymentId = "9a36f589572c492fbee14bd299173c12", ClientCredentials = new ClientCredentials { ClientId = "xyza7891UOFoUhfvfbKgO2xRCIiuAIjH", ClientSecret = "NArwIQT1laFfsS7fdcN1MKDdgwy490w9MBJsAlHN4QI", }, Flags = PlatformFlags.DisableOverlay, CacheDirectory = null, EncryptionKey = null, IsServer = false, RTCOptions = new RTCOptions(), }; #endif platformInterface = PlatformInterface.Create(ref options); if (platformInterface == null) { Debug.LogError("Failed to create EOS platform interface"); status.AppendLine("Failed to create EOS platform interface"); yield break; } Debug.Log("EOS platform interface successfully created!"); status.AppendLine("EOS platform interface created"); authInterface = platformInterface.GetAuthInterface(); status.AppendLine("Auth interface: " + authInterface); connectInterface = platformInterface.GetConnectInterface(); status.AppendLine("Connect interface: " + connectInterface); lobbyInterface = platformInterface.GetLobbyInterface(); status.AppendLine("Lobby interface: " + lobbyInterface); rtcInterface = platformInterface.GetRTCInterface(); // Real-time communication, needed for audio status.AppendLine("RTC interface: " + rtcInterface); audioInterface = rtcInterface.GetAudioInterface(); status.AppendLine("Audio interface: " + audioInterface); voiceChat = new EOSVoiceChat(lobbyInterface, rtcInterface, audioInterface, () => localProductUserId); voiceChat.OnChatConnected += () => status.AppendLine("Chat lobby successfully connected!"); voiceChat.OnChatConnectionFailed += () => status.AppendLine("Chat lobby connection failed..."); voiceChat.OnChatDisconnected += () => status.AppendLine("Chat lobby disconnected"); voiceChat.OnChatUserJoined += userId => { status.AppendLine($"Chat user {userId} joined"); HandleChatUserJoined(userId); }; voiceChat.OnChatUserLeft += userId => status.AppendLine($"Chat user {userId} left"); #if UNITY_GAMECORE SDK.XUserAddAsync(XUserAddOptions.AddDefaultUserAllowingUI, (hr, userHandle) => { if (HR.FAILED(hr)) { Debug.LogError($"Add Xbox user failed, hresult = 0x{hr:X}"); status.AppendLine("Add Xbox user failed..."); return; } SDK.XUserGetId(userHandle, out ulong xuid); Debug.Log($"Xbox user added with XUID: {xuid}"); status.AppendLine($"Xbox user added"); SDK.XUserGetTokenAndSignatureUtf16Async(userHandle, XUserGetTokenAndSignatureOptions.None, "GET", "https://api.epicgames.dev/", null, null, HandleTokenAndSignature); }); yield return new WaitUntil(() => !string.IsNullOrEmpty(xstsToken)); var loginOptions = new Epic.OnlineServices.Connect.LoginOptions { Credentials = new Epic.OnlineServices.Connect.Credentials { Type = ExternalCredentialType.XblXstsToken, Token = xstsToken, }, }; connectInterface.Login(ref loginOptions, null, HandleConnectResult); #elif UNITY_PS4 || UNITY_PS5 var loggedInUser = PSInput.RefreshUsersDetails(0); // Note: this requires a Presence2 service to be enabled on the game app and activated on the corresponding auth server. // The auth server's client ID needs to be registered as an Identity Provider on the EOS portal. var request = new Authentication.GetIdTokenRequest { UserId = loggedInUser.userId, ClientId = "83c6530f-741e-45ff-af3a-ddb90628a928", ClientSecret = "zvgUjFZ5edoiCYNV", Scope = "openid id_token:psn.basic_claims", }; var requestOp = new AsyncRequest(request) .ContinueWith(antecedent => { if (antecedent?.Request == null || antecedent.Request.Result.apiResult != APIResultTypes.Success) { Debug.LogError($"PSN authentication failed, error = {antecedent?.Request?.Result.ErrorMessage()}"); status.AppendLine("PSN authentication failed..."); return; } status.AppendLine("PSN authenticated"); psnIdToken = antecedent.Request.IdToken; }); Authentication.Schedule(requestOp); yield return new WaitUntil(() => !string.IsNullOrEmpty(psnIdToken)); var loginOptions = new Epic.OnlineServices.Connect.LoginOptions { Credentials = new Epic.OnlineServices.Connect.Credentials { Type = ExternalCredentialType.PsnIdToken, Token = psnIdToken, }, }; connectInterface.Login(ref loginOptions, null, HandleConnectResult); #else var loginOptions = new LoginOptions { Credentials = GetEpicCredentials(), ScopeFlags = AuthScopeFlags.BasicProfile, }; authInterface.Login(ref loginOptions, null, HandleLoginResult); #endif } #if UNITY_GAMECORE private void HandleTokenAndSignature(int hresult, XUserGetTokenAndSignatureUtf16Data tokenAndSignature) { if (HR.FAILED(hresult)) { Debug.LogError($"Xbox Live authentication failed, hresult = 0x{hresult:X}"); status.AppendLine("Xbox Live authentication failed..."); return; } Debug.Log($"Xbox Live authenticated, XSTS token = {tokenAndSignature.Token}"); status.AppendLine("Xbox Live successfully authenticated"); xstsToken = tokenAndSignature.Token; } #endif #if UNITY_STANDALONE private Credentials GetEpicCredentials() // This is platform-specific (actually it's not, we can skip this on consoles) { if (!string.IsNullOrEmpty(devAuthAddress)) { return new Credentials { Id = devAuthAddress, Type = LoginCredentialType.Developer, Token = SystemInfo.deviceName, }; } return new Credentials { Type = LoginCredentialType.AccountPortal, // Use ExternalAuth on console platform }; } private void HandleLoginResult(ref LoginCallbackInfo data) { switch (data.ResultCode) { case Result.Success: localEpicAccountId = data.LocalUserId; Debug.Log("EOS login successful: " + localEpicAccountId); status.AppendLine("EOS login successful: " + localEpicAccountId); var copyTokenOptions = new CopyUserAuthTokenOptions(); authInterface.CopyUserAuthToken(ref copyTokenOptions, localEpicAccountId, out Token? token); if (!token.HasValue) { Debug.Log("EOS login failed, invalid token!"); status.AppendLine("EOS login failed, invalid token!"); break; } Debug.Log($"User auth access token: {token.Value.AccessToken}"); var loginOptions = new Epic.OnlineServices.Connect.LoginOptions { Credentials = new Epic.OnlineServices.Connect.Credentials { Type = ExternalCredentialType.Epic, // Can be XSTS or PSN ID as well, platform-specific Token = token.Value.AccessToken, }, }; connectInterface.Login(ref loginOptions, null, HandleConnectResult); break; default: Debug.Log("EOS login failed, result code = " + data.ResultCode); status.AppendLine("EOS login failed"); break; } } #endif private void HandleConnectResult(ref Epic.OnlineServices.Connect.LoginCallbackInfo data) { switch (data.ResultCode) { case Result.Success: localProductUserId = data.LocalUserId; Debug.Log("Connect successful: " + localProductUserId); status.AppendLine("Connect successful: " + localProductUserId); CreateOrJoinVoiceLobby(); break; case Result.InvalidUser: Debug.Log("Invalid user, creating user..."); status.AppendLine("Invalid user, creating user..."); var createUserOptions = new CreateUserOptions { ContinuanceToken = data.ContinuanceToken }; connectInterface.CreateUser(ref createUserOptions, null, HandleUserCreated); break; default: Debug.Log("Connect failed, result code = " + data.ResultCode); status.AppendLine("Connect failed"); break; } } private void HandleUserCreated(ref CreateUserCallbackInfo data) { switch (data.ResultCode) { case Result.Success: localProductUserId = data.LocalUserId; Debug.Log("User creation successful: " + localProductUserId); status.AppendLine("User creation successful: " + localProductUserId); CreateOrJoinVoiceLobby(); break; default: Debug.Log("User creation failed, result code = " + data.ResultCode); status.AppendLine("User creation failed"); break; } } private void CreateOrJoinVoiceLobby() { voiceChat.ConnectToChat(DebugLobbyId); } void Update() { if (platformInterface != null) platformInterface.Tick(); #if UNITY_GAMECORE SDK.XTaskQueueDispatch(); #elif UNITY_PS4 || UNITY_PS5 Main.Update(); #endif } private void OnDestroy() { if (voiceChat != null) { voiceChat.Dispose(); voiceChat = null; } if (platformInterface != null) { platformInterface.Release(); platformInterface = null; } PlatformInterface.Shutdown(); #if UNITY_EDITOR && UNITY_STANDALONE UnloadLibrary(); #endif } #if UNITY_EDITOR && UNITY_STANDALONE private IntPtr eosLbraryHandle; private void LoadLibrary() { // EOS SDK 1.13+ uses dynamic library binding in the Editor but does not provide any system functions to actually load dynamic libraries, // so we need to provide those ourselves. eosLbraryHandle = EOSNativeHelper.LoadLibrary($@"Assets\Plugins\EpicOnlineServices\Bin\{Config.LibraryName}.dll"); if (eosLbraryHandle == IntPtr.Zero) { throw new Exception("Could not load EOS library!"); } Bindings.Hook(eosLbraryHandle, EOSNativeHelper.GetProcAddress); Debug.Log("Hooked EOS library bindings"); status.AppendLine("Hooked EOS library bindings"); } private void UnloadLibrary() { Debug.Log("Unhooking EOS library bindings"); Bindings.Unhook(); if (eosLbraryHandle != IntPtr.Zero) { EOSNativeHelper.FreeLibrary(eosLbraryHandle); eosLbraryHandle = IntPtr.Zero; } } #endif private void OnEOSLogMessage(ref LogMessage message) { switch (message.Level) { case LogLevel.Fatal: case LogLevel.Error: Debug.LogError(message.Message); break; case LogLevel.Warning: Debug.LogWarning(message.Message); break; default: Debug.Log(message.Message); break; } } private void OnGUI() { int screenHeight = Screen.currentResolution.height; if (screenHeight == 0) screenHeight = 1080; float screenScale = screenHeight / 720f; GUI.matrix = Matrix4x4.Scale(new Vector3(screenScale, screenScale, 1)); GUILayout.Label(status.ToString()); } private void HandleChatUserJoined(ProductUserId productUserId) { if (connectInterface == null || localProductUserId == null) return; Debug.Log($"Chat product user ID {productUserId} joined, querying account mapping..."); // We need to know how to map from remote account GUIDs to Epic's ProductUserId so we can query other players' chat status var queryMappingsOptions = new QueryProductUserIdMappingsOptions { LocalUserId = localProductUserId, ProductUserIds = new[] { productUserId }, }; connectInterface.QueryProductUserIdMappings(ref queryMappingsOptions, productUserId, HandleProductUserIdMappings); var queryExternalOptions = new QueryExternalAccountMappingsOptions { LocalUserId = localProductUserId, AccountIdType = ExternalAccountType.Xbl, ExternalAccountIds = new Utf8String[] { "2814644373145756", "2814654765419620" }, }; connectInterface.QueryExternalAccountMappings(ref queryExternalOptions, null, HandleExternalAccountMappings); } private void HandleExternalAccountMappings(ref QueryExternalAccountMappingsCallbackInfo data) { switch (data.ResultCode) { case Result.Success: foreach (var accountId in new[] {"2814644373145756", "2814654765419620"}) { var getMappingOptions = new GetExternalAccountMappingsOptions { AccountIdType = ExternalAccountType.Xbl, LocalUserId = localProductUserId, TargetExternalUserId = accountId }; var productUserId = connectInterface.GetExternalAccountMapping(ref getMappingOptions); if (productUserId != null) { Debug.Log($"Mapped XBL account ID {accountId} to product user ID: {productUserId}"); } else { Debug.Log($"Failed to map XBL account ID {accountId} to any product user ID"); } } break; default: Debug.LogError($"External account mapping query failed with result code: {data.ResultCode}"); break; } } private readonly Dictionary steamAccountMappings = new Dictionary(); private readonly Dictionary epicAccountMappings = new Dictionary(); private readonly Dictionary xblAccountMappings = new Dictionary(); private readonly Dictionary psnAccountMappings = new Dictionary(); private void HandleProductUserIdMappings(ref QueryProductUserIdMappingsCallbackInfo data) { switch (data.ResultCode) { case Result.Success: var productUserId = (ProductUserId)data.ClientData; var copyOptions = new CopyProductUserInfoOptions { TargetUserId = productUserId }; var result = connectInterface.CopyProductUserInfo(ref copyOptions, out var externalAccountInfo); if (result == Result.Success) { Debug.Log($"External account info for product user {productUserId}: type = {externalAccountInfo?.AccountIdType}, account id = {externalAccountInfo?.AccountId}, display name = {externalAccountInfo?.DisplayName}"); } else { Debug.LogError($"Failed to query external account info for product user {productUserId}"); } break; default: Debug.LogError($"Product user ID mapping query failed with result code: {data.ResultCode}"); break; } } }