Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions examples/audience/Assets/link.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,30 @@ hooks fire under IL2CPP with stripping High.
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />

<!--
Epic auto-detection; mirrors SDK link.xml. Ignored if EOS plugin is not present.
Assembly names vary by PlayEveryWare EOS Unity plugin install method:
com.Epic.OnlineServices — EOS C# bindings (UPM)
com.playeveryware.eos.core — EOSManager (UPM)
PlayEveryWare.EpicOnlineServices / EOSSDK — legacy / manual install

Assembly-level preserve="all" only keeps type declarations; method
metadata requires type-level preserve="all".
-->
<assembly fullname="com.Epic.OnlineServices">
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
</assembly>
<assembly fullname="com.playeveryware.eos.core">
<type fullname="PlayEveryWare.EpicOnlineServices.EOSManager" preserve="all" />
</assembly>
<assembly fullname="EOSSDK">
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
</assembly>
<assembly fullname="PlayEveryWare.EpicOnlineServices">
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
</assembly>
</linker>
112 changes: 112 additions & 0 deletions src/Packages/Audience/Runtime/ImmutableAudience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ public static void Init(AudienceConfig config)

FireGameLaunch(config, consentAtInit, skanRegistered, attributionContext);
TryIdentifySteamUser();
TryIdentifyEpicUser();

CheckAndFireAttStatusChanged(config, consentAtInit);

Expand Down Expand Up @@ -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<string, object> 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,
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions src/Packages/Audience/Runtime/Utility/Log.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
}
}
28 changes: 28 additions & 0 deletions src/Packages/Audience/link.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,32 @@ framework dependency.
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />

<!--
Epic platform auto-detection. The SDK resolves these types via reflection
at runtime; entries for missing assemblies are silently ignored.
Assembly names vary by how the PlayEveryWare EOS Unity plugin is installed:
com.Epic.OnlineServices — EOS C# bindings (UPM package install)
com.playeveryware.eos.core — EOSManager (UPM package install)
PlayEveryWare.EpicOnlineServices — custom/manual assembly name
EOSSDK — legacy / direct DLL install

NOTE: assembly-level preserve="all" only keeps type declarations; method
metadata requires type-level preserve="all".
-->
<assembly fullname="com.Epic.OnlineServices">
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
</assembly>
<assembly fullname="EOSSDK">
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
</assembly>
<assembly fullname="PlayEveryWare.EpicOnlineServices">
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
</assembly>
<assembly fullname="com.playeveryware.eos.core">
<type fullname="PlayEveryWare.EpicOnlineServices.EOSManager" preserve="all" />
</assembly>
</linker>
Loading