Skip to content

Commit 17836b1

Browse files
nattb8claude
andcommitted
feat(audience): auto-collect Epic account ID and detect Epic platform
When the PlayEveryWare EOS Unity plugin is present, the SDK now: - Calls Identify() with the logged-in EpicAccountId automatically at Init - Sets distribution_platform = "epic" in game_launch properties when the game was launched from the Epic Games Store Detection uses C# reflection so there is no hard compile-time dependency; games without EOS are completely unaffected. Both EOS Unity plugin install methods are supported: - UPM package (com.playeveryware.eos / com.Epic.OnlineServices assembly) - Legacy DLL install (EOSSDK / PlayEveryWare.EpicOnlineServices assembly) EGS detection is based on launcher-injected CLI args (-EpicPortal, -epicapp=, -epicenv=), not EOS initialisation, so Steam-only games that use EOS for cross-play are not misattributed. link.xml: type-level preserve="all" entries for AuthInterface and EpicAccountId prevent IL2CPP High stripping from removing the method metadata that reflection depends on. Assembly-level preserve="all" only protects type declarations, not method bodies or their metadata. examples/audience/Assets/link.xml mirrors the same rules because Unity's linker skips package link.xml when the package is a file: path (dev mode). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dc64354 commit 17836b1

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

examples/audience/Assets/link.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,30 @@ hooks fire under IL2CPP with stripping High.
3434
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
3535
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
3636
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />
37+
38+
<!--
39+
Epic auto-detection; mirrors SDK link.xml. Ignored if EOS plugin is not present.
40+
Assembly names vary by PlayEveryWare EOS Unity plugin install method:
41+
com.Epic.OnlineServices — EOS C# bindings (UPM)
42+
com.playeveryware.eos.core — EOSManager (UPM)
43+
PlayEveryWare.EpicOnlineServices / EOSSDK — legacy / manual install
44+
45+
Assembly-level preserve="all" only keeps type declarations; method
46+
metadata requires type-level preserve="all".
47+
-->
48+
<assembly fullname="com.Epic.OnlineServices">
49+
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
50+
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
51+
</assembly>
52+
<assembly fullname="com.playeveryware.eos.core">
53+
<type fullname="PlayEveryWare.EpicOnlineServices.EOSManager" preserve="all" />
54+
</assembly>
55+
<assembly fullname="EOSSDK">
56+
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
57+
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
58+
</assembly>
59+
<assembly fullname="PlayEveryWare.EpicOnlineServices">
60+
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
61+
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
62+
</assembly>
3763
</linker>

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ public static void Init(AudienceConfig config)
257257

258258
FireGameLaunch(config, consentAtInit, skanRegistered, attributionContext);
259259
TryIdentifySteamUser();
260+
TryIdentifyEpicUser();
260261

261262
CheckAndFireAttStatusChanged(config, consentAtInit);
262263

@@ -1214,6 +1215,116 @@ private static bool TryGetFacepunchId(out string? id)
12141215
return true;
12151216
}
12161217

1218+
// Resolves PlayEveryWare.EpicOnlineServices.EOSManager across install methods.
1219+
// Returns null when the EOS Unity plugin is not present.
1220+
private static System.Type? ResolveEosManagerType() =>
1221+
System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, PlayEveryWare.EpicOnlineServices")
1222+
?? System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, com.playeveryware.eos.core")
1223+
?? System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, Assembly-CSharp");
1224+
1225+
// Gets the initialised PlatformInterface handle from EOSManager.Instance.
1226+
// Returns null when the EOS plugin is absent or EOS has not been initialised.
1227+
private static object? GetEosPlatformInterface()
1228+
{
1229+
var managerType = ResolveEosManagerType();
1230+
if (managerType == null) return null;
1231+
// Use the compiled getter name (get_Instance) for IL2CPP compatibility;
1232+
// property metadata can be stripped even when the method body survives.
1233+
var instance = managerType.GetMethod("get_Instance")?.Invoke(null, null)
1234+
?? managerType.GetProperty("Instance")?.GetValue(null);
1235+
if (instance == null) return null;
1236+
return instance.GetType().GetMethod("GetEOSPlatformInterface")?.Invoke(instance, null);
1237+
}
1238+
1239+
// Sets distribution_platform = "epic" when the game was launched from the Epic
1240+
// Games Store launcher. Uses command-line args injected by the EGS launcher
1241+
// (-epicenv=, -epicapp=) rather than EOS-being-initialised, which would
1242+
// misattribute Steam games that use EOS purely for cross-play.
1243+
// Config override wins afterward.
1244+
private static void TryDetectEpicPlatform(Dictionary<string, object> properties)
1245+
{
1246+
try
1247+
{
1248+
if (IsLaunchedFromEpicGamesStore())
1249+
properties["distribution_platform"] = DistributionPlatforms.Epic;
1250+
}
1251+
catch (Exception ex)
1252+
{
1253+
Log.Warn(AudienceLogs.EpicPlatformDetectionFailed(ex));
1254+
}
1255+
}
1256+
1257+
// EGS launcher injects these args into every game it launches, regardless of
1258+
// whether the game integrates EOS. Absent when EOS is used for cross-play only.
1259+
// -EpicPortal is the strongest signal (bare flag, no value); -epicapp= and
1260+
// -epicenv= are the environment/artifact identifiers also always present.
1261+
private static bool IsLaunchedFromEpicGamesStore()
1262+
{
1263+
var args = Environment.GetCommandLineArgs();
1264+
foreach (var arg in args)
1265+
{
1266+
if (arg.Equals("-EpicPortal", StringComparison.OrdinalIgnoreCase) ||
1267+
arg.StartsWith("-epicenv=", StringComparison.OrdinalIgnoreCase) ||
1268+
arg.StartsWith("-epicapp=", StringComparison.OrdinalIgnoreCase))
1269+
return true;
1270+
}
1271+
return false;
1272+
}
1273+
1274+
// Calls Identify with the logged-in EOS ProductUserId.
1275+
// No-op if EOS is not present, not initialised, no user is logged in,
1276+
// or consent is below Full.
1277+
private static void TryIdentifyEpicUser()
1278+
{
1279+
try
1280+
{
1281+
if (!TryGetEpicAccountId(out var id))
1282+
return;
1283+
Log.Debug(AudienceLogs.EpicAutoIdentified(id!));
1284+
Identify(id!, IdentityType.Epic);
1285+
}
1286+
catch (Exception ex)
1287+
{
1288+
Log.Warn(AudienceLogs.EpicIdentityCollectionFailed(ex));
1289+
}
1290+
}
1291+
1292+
// Reads the EOS EpicAccountId via AuthInterface.GetLoggedInAccountByIndex(0).
1293+
// EpicAccountId is the player's Epic Games Account — consistent across all products.
1294+
// Requires the game to have already initialised EOS via EOSManager.
1295+
private static bool TryGetEpicAccountId(out string? id)
1296+
{
1297+
id = null;
1298+
1299+
// Guard: EOS C# bindings must be present (assembly name varies by install method).
1300+
if (System.Type.GetType("Epic.OnlineServices.Auth.AuthInterface, com.Epic.OnlineServices") == null
1301+
&& System.Type.GetType("Epic.OnlineServices.Auth.AuthInterface, EOSSDK") == null)
1302+
return false;
1303+
1304+
var platformInterface = GetEosPlatformInterface();
1305+
if (platformInterface == null) return false;
1306+
1307+
var authInterface = platformInterface.GetType()
1308+
.GetMethod("GetAuthInterface")?.Invoke(platformInterface, null);
1309+
if (authInterface == null) return false;
1310+
1311+
// Skip if no accounts are logged in.
1312+
var countResult = authInterface.GetType()
1313+
.GetMethod("GetLoggedInAccountsCount")?.Invoke(authInterface, null);
1314+
if (!(countResult is int count && count > 0)) return false;
1315+
1316+
// C# binding takes a plain int index, not an options struct.
1317+
var epicAccountId = authInterface.GetType()
1318+
.GetMethod("GetLoggedInAccountByIndex")?.Invoke(authInterface, new object[] { 0 });
1319+
if (epicAccountId == null) return false;
1320+
1321+
if (epicAccountId.GetType().GetMethod("IsValid")?.Invoke(epicAccountId, null) as bool? != true)
1322+
return false;
1323+
1324+
id = epicAccountId.ToString();
1325+
return !string.IsNullOrEmpty(id);
1326+
}
1327+
12171328
// consentAtInit only gates the launch; Track still checks live _state via CanTrack.
12181329
private static void FireGameLaunch(
12191330
AudienceConfig config,
@@ -1246,6 +1357,7 @@ private static void FireGameLaunch(
12461357

12471358
// Auto-detect distribution platform via reflection. Config override wins below.
12481359
TryDetectSteamPlatform(properties);
1360+
TryDetectEpicPlatform(properties);
12491361

12501362
// Config-supplied distributionPlatform overrides the auto-detected value.
12511363
if (config.DistributionPlatform != null)

src/Packages/Audience/Runtime/Utility/Log.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,18 @@ internal static string SteamAutoIdentified(string steamId) =>
171171
internal static string SteamIdentityCollectionFailed(Exception ex) =>
172172
$"Steam identity collection threw {ex.GetType().Name}: {ex.Message}. " +
173173
"Steam user ID will not be auto-collected.";
174+
175+
// ---- Epic auto-detection ----
176+
177+
internal static string EpicPlatformDetectionFailed(Exception ex) =>
178+
$"Epic platform detection threw {ex.GetType().Name}: {ex.Message}. " +
179+
"distribution_platform will not be auto-set.";
180+
181+
internal static string EpicAutoIdentified(string epicId) =>
182+
$"auto-identified epic user: {epicId}";
183+
184+
internal static string EpicIdentityCollectionFailed(Exception ex) =>
185+
$"Epic identity collection threw {ex.GetType().Name}: {ex.Message}. " +
186+
"Epic user ID will not be auto-collected.";
174187
}
175188
}

src/Packages/Audience/link.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,32 @@ framework dependency.
4141
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
4242
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
4343
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />
44+
45+
<!--
46+
Epic platform auto-detection. The SDK resolves these types via reflection
47+
at runtime; entries for missing assemblies are silently ignored.
48+
Assembly names vary by how the PlayEveryWare EOS Unity plugin is installed:
49+
com.Epic.OnlineServices — EOS C# bindings (UPM package install)
50+
com.playeveryware.eos.core — EOSManager (UPM package install)
51+
PlayEveryWare.EpicOnlineServices — custom/manual assembly name
52+
EOSSDK — legacy / direct DLL install
53+
54+
NOTE: assembly-level preserve="all" only keeps type declarations; method
55+
metadata requires type-level preserve="all".
56+
-->
57+
<assembly fullname="com.Epic.OnlineServices">
58+
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
59+
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
60+
</assembly>
61+
<assembly fullname="EOSSDK">
62+
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
63+
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
64+
</assembly>
65+
<assembly fullname="PlayEveryWare.EpicOnlineServices">
66+
<type fullname="Epic.OnlineServices.Auth.AuthInterface" preserve="all" />
67+
<type fullname="Epic.OnlineServices.EpicAccountId" preserve="all" />
68+
</assembly>
69+
<assembly fullname="com.playeveryware.eos.core">
70+
<type fullname="PlayEveryWare.EpicOnlineServices.EOSManager" preserve="all" />
71+
</assembly>
4472
</linker>

0 commit comments

Comments
 (0)