diff --git a/examples/audience/Assets/link.xml b/examples/audience/Assets/link.xml index c90feed3..76a2a7c1 100644 --- a/examples/audience/Assets/link.xml +++ b/examples/audience/Assets/link.xml @@ -34,4 +34,8 @@ hooks fire under IL2CPP with stripping High. + + + + diff --git a/src/Packages/Audience/Runtime/ImmutableAudience.cs b/src/Packages/Audience/Runtime/ImmutableAudience.cs index 14c1ba4c..0d8339aa 100644 --- a/src/Packages/Audience/Runtime/ImmutableAudience.cs +++ b/src/Packages/Audience/Runtime/ImmutableAudience.cs @@ -257,6 +257,7 @@ public static void Init(AudienceConfig config) FireGameLaunch(config, consentAtInit, skanRegistered, attributionContext); TryIdentifySteamUser(); + TryIdentifyEpicUser(); CheckAndFireAttStatusChanged(config, consentAtInit); @@ -1214,6 +1215,98 @@ private static bool TryGetFacepunchId(out string? id) return true; } + // Resolves PlayEveryWare.EpicOnlineServices.EOSManager across install methods. + // Returns null when the EOS Unity plugin is not present. + private static System.Type? ResolveEosManagerType() => + System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, PlayEveryWare.EpicOnlineServices") + ?? System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, Assembly-CSharp"); + + // Gets the initialised PlatformInterface handle from EOSManager.Instance. + // Returns null when the EOS plugin is absent or EOS has not been initialised. + private static object? GetEosPlatformInterface() + { + var managerType = ResolveEosManagerType(); + if (managerType == null) return null; + // Use the compiled getter name (get_Instance) for IL2CPP compatibility; + // property metadata can be stripped even when the method body survives. + var instance = managerType.GetMethod("get_Instance")?.Invoke(null, null) + ?? managerType.GetProperty("Instance")?.GetValue(null); + if (instance == null) return null; + return instance.GetType().GetMethod("GetEOSPlatformInterface")?.Invoke(instance, null); + } + + // Sets distribution_platform = "epic" when EOS is active at launch. + // Config override wins afterward. + private static void TryDetectEpicPlatform(Dictionary properties) + { + try + { + if (GetEosPlatformInterface() != null) + properties["distribution_platform"] = DistributionPlatforms.Epic; + } + catch (Exception ex) + { + Log.Warn(AudienceLogs.EpicPlatformDetectionFailed(ex)); + } + } + + // Calls Identify with the logged-in EOS ProductUserId. + // No-op if EOS is not present, not initialised, no user is logged in, + // or consent is below Full. + private static void TryIdentifyEpicUser() + { + try + { + if (!TryGetEpicAccountId(out var id)) + return; + Log.Debug(AudienceLogs.EpicAutoIdentified(id!)); + Identify(id!, IdentityType.Epic); + } + catch (Exception ex) + { + Log.Warn(AudienceLogs.EpicIdentityCollectionFailed(ex)); + } + } + + // Reads the EOS ProductUserId via ConnectInterface.GetLoggedInUserByIndex(0). + // Requires the game to have already initialised EOS via EOSManager. + private static bool TryGetEpicAccountId(out string? id) + { + id = null; + + // Guard: EOSSDK types must be present. + if (System.Type.GetType("Epic.OnlineServices.Connect.ConnectInterface, EOSSDK") == null) + return false; + + var platformInterface = GetEosPlatformInterface(); + if (platformInterface == null) return false; + + var connectInterface = platformInterface.GetType() + .GetMethod("GetConnectInterface")?.Invoke(platformInterface, null); + if (connectInterface == null) return false; + + // Skip if no users are logged in. + var countResult = connectInterface.GetType() + .GetMethod("GetLoggedInUsersCount")?.Invoke(connectInterface, null); + if (!(countResult is int count && count > 0)) return false; + + var optionsType = System.Type.GetType( + "Epic.OnlineServices.Connect.GetLoggedInUserByIndexOptions, EOSSDK"); + if (optionsType == null) return false; + + // Default-constructed options: UserIndex = 0 (first logged-in user). + var options = Activator.CreateInstance(optionsType); + var productUserId = connectInterface.GetType() + .GetMethod("GetLoggedInUserByIndex")?.Invoke(connectInterface, new[] { options }); + if (productUserId == null) return false; + + if (productUserId.GetType().GetMethod("IsValid")?.Invoke(productUserId, null) as bool? != true) + return false; + + id = productUserId.ToString(); + return !string.IsNullOrEmpty(id); + } + // consentAtInit only gates the launch; Track still checks live _state via CanTrack. private static void FireGameLaunch( AudienceConfig config, @@ -1246,6 +1339,7 @@ private static void FireGameLaunch( // Auto-detect distribution platform via reflection. Config override wins below. TryDetectSteamPlatform(properties); + TryDetectEpicPlatform(properties); // Config-supplied distributionPlatform overrides the auto-detected value. if (config.DistributionPlatform != null) diff --git a/src/Packages/Audience/Runtime/Utility/Log.cs b/src/Packages/Audience/Runtime/Utility/Log.cs index 8fa5eb43..7285c5e4 100644 --- a/src/Packages/Audience/Runtime/Utility/Log.cs +++ b/src/Packages/Audience/Runtime/Utility/Log.cs @@ -171,5 +171,18 @@ internal static string SteamAutoIdentified(string steamId) => internal static string SteamIdentityCollectionFailed(Exception ex) => $"Steam identity collection threw {ex.GetType().Name}: {ex.Message}. " + "Steam user ID will not be auto-collected."; + + // ---- Epic auto-detection ---- + + internal static string EpicPlatformDetectionFailed(Exception ex) => + $"Epic platform detection threw {ex.GetType().Name}: {ex.Message}. " + + "distribution_platform will not be auto-set."; + + internal static string EpicAutoIdentified(string epicId) => + $"auto-identified epic user: {epicId}"; + + internal static string EpicIdentityCollectionFailed(Exception ex) => + $"Epic identity collection threw {ex.GetType().Name}: {ex.Message}. " + + "Epic user ID will not be auto-collected."; } } diff --git a/src/Packages/Audience/link.xml b/src/Packages/Audience/link.xml index 38cd8398..2104ffe7 100644 --- a/src/Packages/Audience/link.xml +++ b/src/Packages/Audience/link.xml @@ -41,4 +41,11 @@ framework dependency. + + + +