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.
+
+
+
+