You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

625 lines
22 KiB

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<Authentication.GetIdTokenRequest>(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<string, ProductUserId> steamAccountMappings = new Dictionary<string, ProductUserId>();
private readonly Dictionary<string, ProductUserId> epicAccountMappings = new Dictionary<string, ProductUserId>();
private readonly Dictionary<string, ProductUserId> xblAccountMappings = new Dictionary<string, ProductUserId>();
private readonly Dictionary<string, ProductUserId> psnAccountMappings = new Dictionary<string, ProductUserId>();
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;
}
}
}