diff --git a/examples/audience/Assets/link.xml b/examples/audience/Assets/link.xml index c90feed3..a2a3e288 100644 --- a/examples/audience/Assets/link.xml +++ b/examples/audience/Assets/link.xml @@ -34,4 +34,30 @@ 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..50f6dd01 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,116 @@ 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, com.playeveryware.eos.core") + ?? 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 the game was launched from the Epic + // Games Store launcher. Uses command-line args injected by the EGS launcher + // (-epicenv=, -epicapp=) rather than EOS-being-initialised, which would + // misattribute Steam games that use EOS purely for cross-play. + // Config override wins afterward. + private static void TryDetectEpicPlatform(Dictionary properties) + { + try + { + if (IsLaunchedFromEpicGamesStore()) + properties["distribution_platform"] = DistributionPlatforms.Epic; + } + catch (Exception ex) + { + Log.Warn(AudienceLogs.EpicPlatformDetectionFailed(ex)); + } + } + + // EGS launcher injects these args into every game it launches, regardless of + // whether the game integrates EOS. Absent when EOS is used for cross-play only. + // -EpicPortal is the strongest signal (bare flag, no value); -epicapp= and + // -epicenv= are the environment/artifact identifiers also always present. + private static bool IsLaunchedFromEpicGamesStore() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + if (arg.Equals("-EpicPortal", StringComparison.OrdinalIgnoreCase) || + arg.StartsWith("-epicenv=", StringComparison.OrdinalIgnoreCase) || + arg.StartsWith("-epicapp=", StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + // 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 EpicAccountId via AuthInterface.GetLoggedInAccountByIndex(0). + // EpicAccountId is the player's Epic Games Account — consistent across all products. + // Requires the game to have already initialised EOS via EOSManager. + private static bool TryGetEpicAccountId(out string? id) + { + id = null; + + // Guard: EOS C# bindings must be present (assembly name varies by install method). + if (System.Type.GetType("Epic.OnlineServices.Auth.AuthInterface, com.Epic.OnlineServices") == null + && System.Type.GetType("Epic.OnlineServices.Auth.AuthInterface, EOSSDK") == null) + return false; + + var platformInterface = GetEosPlatformInterface(); + if (platformInterface == null) return false; + + var authInterface = platformInterface.GetType() + .GetMethod("GetAuthInterface")?.Invoke(platformInterface, null); + if (authInterface == null) return false; + + // Skip if no accounts are logged in. + var countResult = authInterface.GetType() + .GetMethod("GetLoggedInAccountsCount")?.Invoke(authInterface, null); + if (!(countResult is int count && count > 0)) return false; + + // C# binding takes a plain int index, not an options struct. + var epicAccountId = authInterface.GetType() + .GetMethod("GetLoggedInAccountByIndex")?.Invoke(authInterface, new object[] { 0 }); + if (epicAccountId == null) return false; + + if (epicAccountId.GetType().GetMethod("IsValid")?.Invoke(epicAccountId, null) as bool? != true) + return false; + + id = epicAccountId.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 +1357,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..22646dc7 100644 --- a/src/Packages/Audience/link.xml +++ b/src/Packages/Audience/link.xml @@ -41,4 +41,32 @@ framework dependency. + + + + + + + + + + + + + + + + +